From bf136d58e543135d7e1027184c4e67acc6f4fd2e Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Fri, 14 Dec 2018 15:22:31 +0100 Subject: [PATCH 001/384] Prod ready (#2) Refactoring --- .gitignore | 9 +- README.md | 0 analysis_options.yaml | 74 +++++++ example/.gitignore | 73 +++++++ example/README.md | 16 ++ example/lib/main.dart | 73 +++++-- example/lib/main.g.dart | 27 +++ example/pubspec.lock | 311 ++++++++++++++++++++++++++- example/pubspec.yaml | 4 +- lib/hook.dart | 128 +---------- lib/src/hook.dart | 9 + lib/src/hook_impl.dart | 329 +++++++++++++++++++++++++++++ lib/src/hook_widget.dart | 410 ++++++++++++++++++++++++++++++++++++ lib/src/stateful_hook.dart | 297 ++++++++++++++++++++++++++ pubspec.lock | 14 ++ pubspec.yaml | 2 + test/hook_builder_test.dart | 27 +++ test/hook_test.dart | 7 - test/hook_widget_test.dart | 25 +++ test/mock.dart | 27 +++ 20 files changed, 1702 insertions(+), 160 deletions(-) create mode 100644 README.md create mode 100644 example/.gitignore create mode 100644 example/README.md create mode 100644 example/lib/main.g.dart create mode 100644 lib/src/hook.dart create mode 100644 lib/src/hook_impl.dart create mode 100644 lib/src/hook_widget.dart create mode 100644 lib/src/stateful_hook.dart create mode 100644 test/hook_builder_test.dart delete mode 100644 test/hook_test.dart create mode 100644 test/hook_widget_test.dart create mode 100644 test/mock.dart diff --git a/.gitignore b/.gitignore index 9ac9d889..e9e8b166 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,10 @@ +# Flutter/Dart/Pub related +**/doc/api/ +.dart_tool/ +.flutter-plugins +.packages +.pub-cache/ +.pub/ +build/ android/ ios/ -.packages \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 00000000..e69de29b diff --git a/analysis_options.yaml b/analysis_options.yaml index 064c6286..233a180f 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -1,9 +1,83 @@ +include: package:pedantic/analysis_options.yaml +analyzer: + strong-mode: + implicit-casts: false + implicit-dynamic: false + errors: + strong_mode_implicit_dynamic_list_literal: ignore + strong_mode_implicit_dynamic_map_literal: ignore + strong_mode_implicit_dynamic_parameter: ignore + strong_mode_implicit_dynamic_variable: ignore linter: rules: + - public_member_api_docs + - annotate_overrides + - avoid_empty_else + - avoid_function_literals_in_foreach_calls + - avoid_init_to_null + - avoid_null_checks_in_equality_operators + - avoid_relative_lib_imports + - avoid_renaming_method_parameters + - avoid_return_types_on_setters + - avoid_returning_null + - avoid_types_as_parameter_names + - avoid_unused_constructor_parameters + - await_only_futures + - camel_case_types - cancel_subscriptions + - cascade_invocations + - comment_references + - constant_identifier_names + - control_flow_in_finally + - directives_ordering + - empty_catches + - empty_constructor_bodies + - empty_statements - hash_and_equals + - implementation_imports + - invariant_booleans - iterable_contains_unrelated_type + - library_names + - library_prefixes - list_remove_unrelated_type + - no_adjacent_strings_in_list + - no_duplicate_case_values + - non_constant_identifier_names + - null_closures + - omit_local_variable_types + - only_throw_errors + - overridden_fields + - package_api_docs + - package_names + - package_prefixed_library_names + - prefer_adjacent_string_concatenation + - prefer_collection_literals + - prefer_conditional_assignment + - prefer_const_constructors + - prefer_contains + - prefer_equal_for_default_values + - prefer_final_fields + - prefer_initializing_formals + - prefer_interpolation_to_compose_strings + - prefer_is_empty + - prefer_is_not_empty + - prefer_single_quotes + - prefer_typing_uninitialized_variables + - recursive_getters + - slash_for_doc_comments + - super_goes_last - test_types_in_equals + - throw_in_finally + - type_init_formals + - unawaited_futures + - unnecessary_brace_in_string_interps + - unnecessary_const + - unnecessary_getters_setters + - unnecessary_lambdas + - unnecessary_new + - unnecessary_null_aware_assignments + - unnecessary_statements + - unnecessary_this - unrelated_type_equality_checks + - use_rethrow_when_possible - valid_regexps diff --git a/example/.gitignore b/example/.gitignore new file mode 100644 index 00000000..4f842a89 --- /dev/null +++ b/example/.gitignore @@ -0,0 +1,73 @@ +# Miscellaneous +*.class +*.lock +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ +/test +.metadata + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# Visual Studio Code related +.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +.dart_tool/ +.flutter-plugins +.packages +.pub-cache/ +.pub/ +build/ + +# Android related +**/android/**/gradle-wrapper.jar +**/android/.gradle +**/android/captures/ +**/android/gradlew +**/android/gradlew.bat +**/android/local.properties +**/android/**/GeneratedPluginRegistrant.java + +# iOS/XCode related +**/ios/**/*.mode1v3 +**/ios/**/*.mode2v3 +**/ios/**/*.moved-aside +**/ios/**/*.pbxuser +**/ios/**/*.perspectivev3 +**/ios/**/*sync/ +**/ios/**/.sconsign.dblite +**/ios/**/.tags* +**/ios/**/.vagrant/ +**/ios/**/DerivedData/ +**/ios/**/Icon? +**/ios/**/Pods/ +**/ios/**/.symlinks/ +**/ios/**/profile +**/ios/**/xcuserdata +**/ios/.generated/ +**/ios/Flutter/App.framework +**/ios/Flutter/Flutter.framework +**/ios/Flutter/Generated.xcconfig +**/ios/Flutter/app.flx +**/ios/Flutter/app.zip +**/ios/Flutter/flutter_assets/ +**/ios/ServiceDefinitions.json +**/ios/Runner/GeneratedPluginRegistrant.* + +# Exceptions to above rules. +!**/ios/**/default.mode1v3 +!**/ios/**/default.mode2v3 +!**/ios/**/default.pbxuser +!**/ios/**/default.perspectivev3 +!/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages diff --git a/example/README.md b/example/README.md new file mode 100644 index 00000000..6d17fb36 --- /dev/null +++ b/example/README.md @@ -0,0 +1,16 @@ +# example + +A new Flutter project. + +## Getting Started + +This project is a starting point for a Flutter application. + +A few resources to get you started if this is your first Flutter project: + +- [Lab: Write your first Flutter app](https://flutter.io/docs/get-started/codelab) +- [Cookbook: Useful Flutter samples](https://flutter.io/docs/cookbook) + +For help getting started with Flutter, view our +[online documentation](https://flutter.io/docs), which offers tutorials, +samples, guidance on mobile development, and a full API reference. diff --git a/example/lib/main.dart b/example/lib/main.dart index da1497bd..ef6bf8fd 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -1,6 +1,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_hooks/hook.dart'; -import 'package:rxdart/rxdart.dart'; +import 'package:functional_widget_annotation/functional_widget_annotation.dart'; + +part 'main.g.dart'; void main() => runApp(MyApp()); @@ -8,28 +10,61 @@ class MyApp extends StatelessWidget { // This widget is the root of your application. @override Widget build(BuildContext context) { - return MaterialApp(title: 'Flutter Demo', home: Home()); + return const MaterialApp( + title: 'Flutter Demo', + home: _Foo(), + ); } } -Observable controller = - Observable.periodic(const Duration(seconds: 1), (i) => i); -Observable controller2 = - Observable.periodic(const Duration(seconds: 2), (i) => i); +@widget +Widget _testAnimation(HookContext context, {Color color}) { + final controller = + context.useAnimationController(duration: const Duration(seconds: 5)); -class Home extends HookWidget { - @override - Widget build(HookContext context) { - final v1 = context.useStream(controller); - final v2 = context.useStream(controller2); - return Scaffold( - appBar: AppBar(), - body: Column( - children: [ - Text(v1.data?.toString() ?? "0"), - Text(v2.data?.toString() ?? "0"), + final colorTween = context.useValueChanged( + color, + (Color oldValue, Animation oldResult) { + return ColorTween( + begin: oldResult?.value ?? oldValue, + end: color, + ).animate(controller..forward(from: 0)); + }, + ) ?? + AlwaysStoppedAnimation(color); + + final currentColor = context.useAnimation(colorTween); + return Container(color: currentColor); +} + +@widget +Widget _foo(HookContext context) { + final toggle = context.useState(initialData: false); + final counter = context.useState(initialData: 0); + + return Scaffold( + body: GestureDetector( + onTap: () { + toggle.value = !toggle.value; + }, + child: Stack( + children: [ + Positioned.fill( + child: _TestAnimation( + color: toggle.value + ? const Color.fromARGB(255, 255, 0, 0) + : const Color.fromARGB(255, 0, 0, 255), + ), + ), + Center( + child: Text(counter.value.toString()), + ) ], ), - ); - } + ), + floatingActionButton: FloatingActionButton( + onPressed: () => counter.value++, + child: const Icon(Icons.plus_one), + ), + ); } diff --git a/example/lib/main.g.dart b/example/lib/main.g.dart new file mode 100644 index 00000000..b65c6ace --- /dev/null +++ b/example/lib/main.g.dart @@ -0,0 +1,27 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'main.dart'; + +// ************************************************************************** +// Generator: FunctionalWidget +// ************************************************************************** + +class _TestAnimation extends HookWidget { + const _TestAnimation({Key key, this.color}) : super(key: key); + + final Color color; + + @override + Widget build(HookContext _context) { + return _testAnimation(_context, color: color); + } +} + +class _Foo extends HookWidget { + const _Foo({Key key}) : super(key: key); + + @override + Widget build(HookContext _context) { + return _foo(_context); + } +} diff --git a/example/pubspec.lock b/example/pubspec.lock index 01f7f08e..291cee91 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -1,6 +1,20 @@ # Generated by pub # See https://www.dartlang.org/tools/pub/glossary#lockfile packages: + analyzer: + dependency: transitive + description: + name: analyzer + url: "https://pub.dartlang.org" + source: hosted + version: "0.33.6" + args: + dependency: transitive + description: + name: args + url: "https://pub.dartlang.org" + source: hosted + version: "1.5.1" async: dependency: transitive description: @@ -15,6 +29,55 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.0.4" + build: + dependency: transitive + description: + name: build + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.1" + build_config: + dependency: transitive + description: + name: build_config + url: "https://pub.dartlang.org" + source: hosted + version: "0.3.1+4" + build_resolvers: + dependency: transitive + description: + name: build_resolvers + url: "https://pub.dartlang.org" + source: hosted + version: "0.2.2+6" + build_runner: + dependency: "direct dev" + description: + name: build_runner + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.2" + build_runner_core: + dependency: transitive + description: + name: build_runner_core + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.1" + built_collection: + dependency: transitive + description: + name: built_collection + url: "https://pub.dartlang.org" + source: hosted + version: "4.1.0" + built_value: + dependency: transitive + description: + name: built_value + url: "https://pub.dartlang.org" + source: hosted + version: "6.1.6" charcode: dependency: transitive description: @@ -22,6 +85,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.1.2" + code_builder: + dependency: transitive + description: + name: code_builder + url: "https://pub.dartlang.org" + source: hosted + version: "3.1.3" collection: dependency: transitive description: @@ -29,6 +99,41 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.14.11" + convert: + dependency: transitive + description: + name: convert + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.2" + crypto: + dependency: transitive + description: + name: crypto + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.6" + csslib: + dependency: transitive + description: + name: csslib + url: "https://pub.dartlang.org" + source: hosted + version: "0.14.6" + dart_style: + dependency: transitive + description: + name: dart_style + url: "https://pub.dartlang.org" + source: hosted + version: "1.2.0" + fixnum: + dependency: transitive + description: + name: fixnum + url: "https://pub.dartlang.org" + source: hosted + version: "0.10.9" flutter: dependency: "direct main" description: flutter @@ -46,6 +151,97 @@ packages: description: flutter source: sdk version: "0.0.0" + front_end: + dependency: transitive + description: + name: front_end + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.6+8" + functional_widget: + dependency: "direct dev" + description: + name: functional_widget + url: "https://pub.dartlang.org" + source: hosted + version: "0.2.1" + functional_widget_annotation: + dependency: "direct main" + description: + name: functional_widget_annotation + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.0" + glob: + dependency: transitive + description: + name: glob + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.7" + graphs: + dependency: transitive + description: + name: graphs + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.3+1" + html: + dependency: transitive + description: + name: html + url: "https://pub.dartlang.org" + source: hosted + version: "0.13.3+3" + http_multi_server: + dependency: transitive + description: + name: http_multi_server + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.5" + http_parser: + dependency: transitive + description: + name: http_parser + url: "https://pub.dartlang.org" + source: hosted + version: "3.1.3" + io: + dependency: transitive + description: + name: io + url: "https://pub.dartlang.org" + source: hosted + version: "0.3.3" + js: + dependency: transitive + description: + name: js + url: "https://pub.dartlang.org" + source: hosted + version: "0.6.1+1" + json_annotation: + dependency: transitive + description: + name: json_annotation + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.0" + kernel: + dependency: transitive + description: + name: kernel + url: "https://pub.dartlang.org" + source: hosted + version: "0.3.6+8" + logging: + dependency: transitive + description: + name: logging + url: "https://pub.dartlang.org" + source: hosted + version: "0.11.3+2" matcher: dependency: transitive description: @@ -60,6 +256,20 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.1.6" + mime: + dependency: transitive + description: + name: mime + url: "https://pub.dartlang.org" + source: hosted + version: "0.9.6+2" + package_config: + dependency: transitive + description: + name: package_config + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.5" path: dependency: transitive description: @@ -67,6 +277,41 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.6.2" + pedantic: + dependency: transitive + description: + name: pedantic + url: "https://pub.dartlang.org" + source: hosted + version: "1.3.0" + plugin: + dependency: transitive + description: + name: plugin + url: "https://pub.dartlang.org" + source: hosted + version: "0.2.0+3" + pool: + dependency: transitive + description: + name: pool + url: "https://pub.dartlang.org" + source: hosted + version: "1.3.6" + pub_semver: + dependency: transitive + description: + name: pub_semver + url: "https://pub.dartlang.org" + source: hosted + version: "1.4.2" + pubspec_parse: + dependency: transitive + description: + name: pubspec_parse + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.2+3" quiver: dependency: transitive description: @@ -74,18 +319,32 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.0.1" - rxdart: - dependency: "direct main" + shelf: + dependency: transitive + description: + name: shelf + url: "https://pub.dartlang.org" + source: hosted + version: "0.7.3+3" + shelf_web_socket: + dependency: transitive description: - name: rxdart + name: shelf_web_socket url: "https://pub.dartlang.org" source: hosted - version: "0.19.0" + version: "0.2.2+4" sky_engine: dependency: transitive description: flutter source: sdk version: "0.0.99" + source_gen: + dependency: transitive + description: + name: source_gen + url: "https://pub.dartlang.org" + source: hosted + version: "0.9.1+3" source_span: dependency: transitive description: @@ -107,6 +366,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.6.8" + stream_transform: + dependency: transitive + description: + name: stream_transform + url: "https://pub.dartlang.org" + source: hosted + version: "0.0.14+1" string_scanner: dependency: transitive description: @@ -128,6 +394,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.2.1" + timing: + dependency: transitive + description: + name: timing + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.1+1" typed_data: dependency: transitive description: @@ -135,6 +408,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.1.6" + utf: + dependency: transitive + description: + name: utf + url: "https://pub.dartlang.org" + source: hosted + version: "0.9.0+5" vector_math: dependency: transitive description: @@ -142,5 +422,26 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.0.8" + watcher: + dependency: transitive + description: + name: watcher + url: "https://pub.dartlang.org" + source: hosted + version: "0.9.7+10" + web_socket_channel: + dependency: transitive + description: + name: web_socket_channel + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.9" + yaml: + dependency: transitive + description: + name: yaml + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.15" sdks: - dart: ">=2.0.0 <3.0.0" + dart: ">=2.1.0-dev.5.0 <3.0.0" diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 3d98afce..7ad07be4 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -9,12 +9,14 @@ environment: dependencies: flutter: sdk: flutter - rxdart: 0.19.0 flutter_hooks: 0.0.1 + functional_widget_annotation: 0.1.0 dev_dependencies: flutter_test: sdk: flutter + functional_widget: 0.2.1 + build_runner: dependency_overrides: flutter_hooks: diff --git a/lib/hook.dart b/lib/hook.dart index 5481e9bf..2e371479 100644 --- a/lib/hook.dart +++ b/lib/hook.dart @@ -1,127 +1 @@ -import 'dart:async'; - -import 'package:flutter/widgets.dart'; - -@immutable -abstract class Hook { - HookState createHookState(); -} - -class HookState { - Element _element; - BuildContext get context => _element; - T _hook; - T get hook => _hook; - void initHook() {} - void dispose() {} - void didUpdateWidget(covariant Widget widget) {} - void didUpdateHook(covariant Hook hook) {} - void setState(VoidCallback callback) { - callback(); - _element.markNeedsBuild(); - } -} - -class _StreamHook extends Hook { - final Stream stream; - final T initialData; - _StreamHook({this.stream, this.initialData}); - @override - HookState createHookState() => _StreamHookState(); -} - -class _StreamHookState extends HookState<_StreamHook> { - StreamSubscription subscription; - AsyncSnapshot snapshot; - @override - void initHook() { - super.initHook(); - snapshot = hook.stream == null - ? AsyncSnapshot.nothing() - : AsyncSnapshot.withData(ConnectionState.waiting, hook.initialData); - subscription = hook.stream.listen(onData, onDone: onDone, onError: onError); - } - - void onData(T event) { - print('on data $event'); - setState(() { - snapshot = AsyncSnapshot.withData(ConnectionState.active, event); - }); - } - - void onDone() { - setState(() { - snapshot = AsyncSnapshot.withData(ConnectionState.done, snapshot.data); - }); - } - - void onError(Object error) { - setState(() { - snapshot = AsyncSnapshot.withError(ConnectionState.active, error); - }); - } - - @override - void dispose() { - subscription?.cancel(); - super.dispose(); - } -} - -class HookElement extends StatelessElement implements HookContext { - int _hooksIndex; - List _hooks; - - HookElement(HookWidget widget) : super(widget); - - @override - HookWidget get widget => super.widget; - - HookState useHook(Hook hook) { - final int hooksIndex = _hooksIndex; - _hooksIndex++; - _hooks ??= []; - - HookState state; - if (hooksIndex >= _hooks.length) { - state = hook.createHookState() - .._element = this - .._hook = hook - ..initHook(); - _hooks.add(state); - } else { - state = _hooks[hooksIndex]; - if (!identical(state._hook, hook)) { - final Hook previousHook = state._hook; - state._hook = hook; - state.didUpdateHook(previousHook); - } - } - return state; - } - - AsyncSnapshot useStream(Stream stream, {T initialData}) { - final _StreamHookState state = - useHook(_StreamHook(stream: stream, initialData: initialData)); - return state.snapshot; - } - - @override - void performRebuild() { - _hooksIndex = 0; - super.performRebuild(); - } -} - -abstract class HookWidget extends StatelessWidget { - @override - HookElement createElement() => HookElement(this); - - @protected - Widget build(covariant HookContext context); -} - -abstract class HookContext extends BuildContext { - HookState useHook(Hook hook); - AsyncSnapshot useStream(Stream stream, {T initialData}); -} +export 'package:flutter_hooks/src/hook.dart'; diff --git a/lib/src/hook.dart b/lib/src/hook.dart new file mode 100644 index 00000000..e27e05b0 --- /dev/null +++ b/lib/src/hook.dart @@ -0,0 +1,9 @@ +import 'dart:async'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/scheduler.dart' show Ticker, TickerCallback; +import 'package:flutter/widgets.dart'; + +part 'hook_widget.dart'; +part 'hook_impl.dart'; diff --git a/lib/src/hook_impl.dart b/lib/src/hook_impl.dart new file mode 100644 index 00000000..7d1a75f8 --- /dev/null +++ b/lib/src/hook_impl.dart @@ -0,0 +1,329 @@ +part of 'hook.dart'; + +class _StreamHook extends Hook> { + final Stream stream; + final T initialData; + _StreamHook({this.stream, this.initialData}); + + @override + _StreamHookState createState() => _StreamHookState(); +} + +class _StreamHookState extends HookState, _StreamHook> { + StreamSubscription subscription; + AsyncSnapshot snapshot; + @override + void initHook() { + super.initHook(); + _listen(hook.stream); + } + + @override + void didUpdateHook(_StreamHook oldHook) { + super.didUpdateHook(oldHook); + if (oldHook.stream != hook.stream) { + _listen(hook.stream); + } + } + + void _listen(Stream stream) { + subscription?.cancel(); + snapshot = stream == null + ? AsyncSnapshot.nothing() + : AsyncSnapshot.withData(ConnectionState.waiting, hook.initialData); + subscription = + hook.stream.listen(_onData, onDone: _onDone, onError: _onError); + } + + void _onData(T event) { + setState(() { + snapshot = AsyncSnapshot.withData(ConnectionState.active, event); + }); + } + + void _onDone() { + setState(() { + snapshot = snapshot.hasError + ? AsyncSnapshot.withError(ConnectionState.active, snapshot.error) + : AsyncSnapshot.withData(ConnectionState.done, snapshot.data); + }); + } + + void _onError(Object error) { + setState(() { + snapshot = AsyncSnapshot.withError(ConnectionState.active, error); + }); + } + + @override + void dispose() { + subscription?.cancel(); + super.dispose(); + } + + @override + AsyncSnapshot build(HookContext context) { + return snapshot; + } +} + +class _TickerProviderHook extends Hook { + const _TickerProviderHook(); + + @override + _TickerProviderHookState createState() => _TickerProviderHookState(); +} + +class _TickerProviderHookState + extends HookState + implements TickerProvider { + Ticker _ticker; + + @override + Ticker createTicker(TickerCallback onTick) { + assert(() { + if (_ticker == null) return true; + // TODO: update error + throw FlutterError( + '$runtimeType is a SingleTickerProviderStateMixin but multiple tickers were created.\n' + 'A SingleTickerProviderStateMixin can only be used as a TickerProvider once. If a ' + 'State is used for multiple AnimationController objects, or if it is passed to other ' + 'objects and those objects might use it more than one time in total, then instead of ' + 'mixing in a SingleTickerProviderStateMixin, use a regular TickerProviderStateMixin.'); + }()); + _ticker = Ticker(onTick, debugLabel: 'created by $this'); + // TODO: check if the following is still valid + // We assume that this is called from initState, build, or some sort of + // event handler, and that thus TickerMode.of(context) would return true. We + // can't actually check that here because if we're in initState then we're + // not allowed to do inheritance checks yet. + return _ticker; + } + + @override + void dispose() { + assert(() { + if (_ticker == null || !_ticker.isActive) return true; + // TODO: update error + throw FlutterError('$this was disposed with an active Ticker.\n' + '$runtimeType created a Ticker via its SingleTickerProviderStateMixin, but at the time ' + 'dispose() was called on the mixin, that Ticker was still active. The Ticker must ' + 'be disposed before calling super.dispose(). Tickers used by AnimationControllers ' + 'should be disposed by calling dispose() on the AnimationController itself. ' + 'Otherwise, the ticker will leak.\n' + 'The offending ticker was: ${_ticker.toString(debugIncludeStack: true)}'); + }()); + super.dispose(); + } + + @override + TickerProvider build(HookContext context) { + if (_ticker != null) _ticker.muted = !TickerMode.of(context); + + return this; + } +} + +class _AnimationControllerHook extends StatelessHook { + final Duration duration; + + const _AnimationControllerHook({this.duration}); + + @override + AnimationController build(HookContext context) { + final tickerProvider = context.useTickerProvider(); + + final animationController = context.useMemoized( + () => AnimationController(vsync: tickerProvider, duration: duration), + dispose: (animationController) => animationController.dispose(), + ); + + context + ..useValueChanged(tickerProvider, (_, __) { + animationController.resync(tickerProvider); + }) + ..useValueChanged(duration, (_, __) { + animationController.duration = duration; + }); + + return animationController; + } +} + +class _MemoizedHook extends Hook { + final T Function() valueBuilder; + final void Function(T value) dispose; + final List parameters; + + const _MemoizedHook(this.valueBuilder, + {this.parameters = const [], this.dispose}) + : assert(valueBuilder != null), + assert(parameters != null); + + @override + _MemoizedHookState createState() => _MemoizedHookState(); +} + +class _MemoizedHookState extends HookState> { + T value; + + @override + void initHook() { + super.initHook(); + value = hook.valueBuilder(); + } + + @override + void didUpdateHook(_MemoizedHook oldHook) { + super.didUpdateHook(oldHook); + if (hook.parameters != oldHook.parameters && + (hook.parameters.length != oldHook.parameters.length || + _hasDiffWith(oldHook.parameters))) { + if (hook.dispose != null) { + hook.dispose(value); + } + value = hook.valueBuilder(); + } + } + + @override + void dispose() { + if (hook.dispose != null) { + hook.dispose(value); + } + super.dispose(); + } + + bool _hasDiffWith(List parameters) { + for (var i = 0; i < parameters.length; i++) { + if (parameters[i] != hook.parameters[i]) { + return true; + } + } + return false; + } + + @override + T build(HookContext context) { + return value; + } +} + +class _ValueChangedHook extends Hook { + final R Function(T oldValue, R oldResult) valueChanged; + final T value; + + const _ValueChangedHook(this.value, this.valueChanged) + : assert(valueChanged != null); + + @override + _ValueChangedHookState createState() => _ValueChangedHookState(); +} + +class _ValueChangedHookState + extends HookState> { + R _result; + + @override + void didUpdateHook(_ValueChangedHook oldHook) { + super.didUpdateHook(oldHook); + if (hook.value != oldHook.value) { + _result = hook.valueChanged(oldHook.value, _result); + } + } + + @override + R build(HookContext context) { + return _result; + } +} + +class _AnimationHook extends Hook { + final Animation animation; + + const _AnimationHook(this.animation) : assert(animation != null); + + @override + _AnimationHookState createState() => _AnimationHookState(); +} + +class _AnimationHookState extends HookState> { + @override + void initHook() { + super.initHook(); + hook.animation.addListener(_listener); + } + + @override + void dispose() { + hook.animation.removeListener(_listener); + super.dispose(); + } + + @override + T build(HookContext context) { + context.useValueChanged(hook.animation, valueChange); + return hook.animation.value; + } + + void _listener() { + setState(() {}); + } + + void valueChange(Animation previous, _) { + previous.removeListener(_listener); + hook.animation.addListener(_listener); + } +} + +class _StateHook extends Hook> { + final T initialData; + final void Function(T value) dispose; + + const _StateHook({this.initialData, this.dispose}); + + @override + _StateHookState createState() => _StateHookState(); +} + +class _StateHookState extends HookState, _StateHook> { + TickerProvider _ticker; + ValueNotifier _state; + + @override + void initHook() { + super.initHook(); + _state = ValueNotifier(hook.initialData)..addListener(_listener); + } + + @override + void dispose() { + if (hook.dispose != null) { + hook.dispose(_state.value); + } + _state.dispose(); + super.dispose(); + } + + @override + ValueNotifier build(HookContext context) { + return _state; + } + + void _listener() { + setState(() {}); + } +} + +class HookBuilder extends HookWidget { + final Widget Function(HookContext context) builder; + + const HookBuilder({ + @required this.builder, + Key key, + }) : assert(builder != null), + super(key: key); + + @override + Widget build(HookContext context) => builder(context); +} diff --git a/lib/src/hook_widget.dart b/lib/src/hook_widget.dart new file mode 100644 index 00000000..fab16865 --- /dev/null +++ b/lib/src/hook_widget.dart @@ -0,0 +1,410 @@ +part of 'hook.dart'; + +/// [Hook] is similar to a [StatelessWidget], but is not associated +/// to an [Element]. +/// +/// A [Hook] is typically the equivalent of [State] for [StatefulWidget], +/// with the notable difference that a [HookWidget] can have more than one [Hook]. +/// A [Hook] is created within the [HookState.build] method of [HookWidget] and the creation +/// must be made unconditionally, always in the same order. +/// +/// ### Good: +/// ``` +/// class Good extends HookWidget { +/// @override +/// Widget build(HookContext context) { +/// final name = context.useState(""); +/// // ... +/// } +/// } +/// ``` +/// +/// ### Bad: +/// ``` +/// class Bad extends HookWidget { +/// @override +/// Widget build(HookContext context) { +/// if (condition) { +/// final name = context.useState(""); +/// // ... +/// } +/// } +/// } +/// ``` +/// +/// The reason for such restriction is that [HookState] are obtained based on their index. +/// So the index must never ever change, or it will lead to undesired behavior. +/// +/// ## The usage +/// +/// [Hook] is powerful tool to reuse [State] logic between multiple [Widget]. +/// They are used to extract logic that depends on a [Widget] life-cycle (such as [HookState.dispose]). +/// +/// While mixins are a good candidate too, they do not allow sharing values. A mixin cannot reasonnably +/// define a variable, as this can lead to variable conflicts on bigger widgets. +/// +/// Hooks are designed so that they get the benefits of mixins, but are totally independent from each others. +/// This means that hooks can store and expose values without fearing that the name is already taken by another mixin. +/// +/// ## Example +/// +/// A common use-case is to handle disposable objects such as [AnimationController]. +/// +/// With the usual [StatefulWidget], we would typically have the following: +/// +/// ``` +/// class Usual extends StatefulWidget { +/// @override +/// _UsualState createState() => _UsualState(); +/// } +/// +/// class _UsualState extends State +/// with SingleTickerProviderStateMixin { +/// AnimationController _controller; +/// +/// @override +/// void initState() { +/// super.initState(); +/// _controller = AnimationController( +/// vsync: this, +/// duration: const Duration(seconds: 1), +/// ); +/// } +/// +/// @override +/// void dispose() { +/// super.dispose(); +/// _controller.dispose(); +/// } +/// +/// @override +/// Widget build(BuildContext context) { +/// return Container( +/// +/// ); +/// } +/// } +/// ``` +/// +/// This is undesired because every single widget that want to use an [AnimationController] will have to +/// rewrite this extact piece of code. +/// +/// With hooks it is possible to extract that exact piece of code into a reusable one. In fact, one is already provided by default: +/// [HookContext.useAnimationController] +/// +/// This means that with [HookWidget] the following code is equivalent to the previous example: +/// +/// ``` +/// class Usual extends HookWidget { +/// @override +/// Widget build(HookContext context) { +/// final animationController = +/// context.useAnimationController(duration: const Duration(seconds: 1)); +/// return Container(); +/// } +/// } +/// ``` +/// +/// This is visibly less code then before. But in this example, the `animationController` is still +/// guaranted to be disposed when the widget is removed from the tree. +/// +/// In fact this has secondary bonus: `duration` is kept updated with the latest value. +/// If we were to pass a variable as `duration` instead of a constant, then on value change the [AnimationController] will be updated. +@immutable +abstract class Hook { + /// Allows subclasses to have a `const` constructor + const Hook(); + + /// Creates the mutable state for this hook linked to its widget creator. + /// + /// Subclasses should override this method to return a newly created instance of their associated State subclass: + /// + /// ``` + /// @override + /// HookState createState() => _MyHookState(); + /// ``` + /// + /// The framework can call this method multiple times over the lifetime of a [HookWidget]. For example, + /// if the hook is used multiple times, a separate [HookState] must be created for each usage. + @protected + HookState> createState(); +} + +/// Tracks the lifecycle of [State] objects when asserts are enabled. +enum _HookLifecycle { + /// The [State] object has been created. [State.initState] is called at this + /// time. + created, + + /// The [State.initState] method has been called but the [State] object is + /// not yet ready to build. [State.didChangeDependencies] is called at this time. + initialized, + + /// The [State] object is ready to build and [State.dispose] has not yet been + /// called. + ready, + + /// The [State.dispose] method has been called and the [State] object is + /// no longer able to build. + defunct, +} + +/// The logic and internal state for a [HookWidget] +/// +/// A [HookState] +abstract class HookState> { + /// Equivalent of [State.context] for [HookState] + @protected + BuildContext get context => _element.context; + State _element; + + /// Equivalent of [State.widget] for [HookState] + T get hook => _hook; + T _hook; + + /// Equivalent of [State.initState] for [HookState] + @protected + void initHook() {} + + /// Equivalent of [State.dispose] for [HookState] + @protected + void dispose() {} + + /// Called everytimes the [HookState] is requested + /// + /// [build] is where an [HookState] may use other hooks. This restriction is made to ensure that hooks are unconditionally always requested + @protected + R build(HookContext context); + + /// Equivalent of [State.didUpdateWidget] for [HookState] + @protected + void didUpdateHook(covariant Hook oldHook) {} + + /// Equivalent of [State.setState] for [HookState] + @protected + void setState(VoidCallback fn) { + // ignore: invalid_use_of_protected_member + _element.setState(fn); + } +} + +class HookElement extends StatefulElement implements HookContext { + Iterator _currentHook; + int _hooksIndex; + List _hooks; + + bool _debugIsBuilding; + bool _didReassemble; + bool _isFirstBuild; + + HookElement(HookWidget widget) : super(widget); + + @override + HookWidget get widget => super.widget as HookWidget; + + @override + void performRebuild() { + _currentHook = _hooks?.iterator; + // first iterator always has null for unknown reasons + _currentHook?.moveNext(); + _hooksIndex = 0; + assert(() { + _isFirstBuild ??= true; + _didReassemble ??= false; + _debugIsBuilding = true; + return true; + }()); + super.performRebuild(); + assert(() { + _isFirstBuild = false; + _didReassemble = false; + _debugIsBuilding = false; + return true; + }()); + } + + @override + void unmount() { + super.unmount(); + if (_hooks != null) { + for (final hook in _hooks) { + try { + hook.dispose(); + } catch (exception, stack) { + FlutterError.reportError(FlutterErrorDetails( + exception: exception, + stack: stack, + library: 'hooks library', + context: 'while disposing ${hook.runtimeType}', + )); + } + } + } + } + + @override + R use(Hook hook) { + assert(_debugIsBuilding == true, ''' + Hooks should only be called within the build method of a widget. + Calling them outside of build method leads to an unstable state and is therefore prohibited + '''); + + HookState> hookState; + // first build + if (_currentHook == null) { + assert(_didReassemble || _isFirstBuild); + hookState = _createHookState(hook); + _hooks ??= []; + _hooks.add(hookState); + } else { + // recreate states on hot-reload of the order changed + assert(() { + if (!_didReassemble) { + return true; + } + if (_currentHook.current?.hook?.runtimeType == hook.runtimeType) { + return true; + } else if (_currentHook.current != null) { + for (var i = _hooks.length - 1; i >= _hooksIndex; i--) { + _hooks.removeLast().dispose(); + } + } + hookState = _createHookState(hook); + _hooks.add(hookState); + _currentHook = _hooks.iterator; + for (var i = 0; i < _hooks.length; i++) { + _currentHook.moveNext(); + } + + return true; + }()); + assert(_currentHook.current.hook.runtimeType == hook.runtimeType); + + hookState = _currentHook.current as HookState>; + _currentHook.moveNext(); + + if (hookState._hook != hook) { + // TODO: compare type for potential reassemble + final Hook previousHook = hookState._hook; + hookState + .._hook = hook + ..didUpdateHook(previousHook); + } + } + + _hooksIndex++; + return hookState.build(this); + } + + HookState> _createHookState(Hook hook) { + return hook.createState() + .._element = state + .._hook = hook + ..initHook(); + } + + AsyncSnapshot useStream(Stream stream, {T initialData}) { + return use(_StreamHook(stream: stream, initialData: initialData)); + } + + @override + ValueNotifier useState({T initialData, void dispose(T value)}) { + return use(_StateHook(initialData: initialData, dispose: dispose)); + } + + @override + T useAnimation(Animation animation) { + return use(_AnimationHook(animation)); + } + + @override + AnimationController useAnimationController({Duration duration}) { + return use(_AnimationControllerHook(duration: duration)); + } + + @override + TickerProvider useTickerProvider() { + return use(const _TickerProviderHook()); + } + + @override + void useListenable(Listenable listenable) { + throw UnimplementedError(); + } + + @override + T useMemoized(T Function() valueBuilder, + {List parameters = const [], void dispose(T value)}) { + return use(_MemoizedHook( + valueBuilder, + dispose: dispose, + parameters: parameters, + )); + } + + @override + R useValueChanged(T value, R valueChange(T oldValue, R oldResult)) { + return use(_ValueChangedHook(value, valueChange)); + } +} + +abstract class StatelessHook extends Hook { + const StatelessHook(); + + R build(HookContext context); + + @override + _StatelessHookState createState() => _StatelessHookState(); +} + +class _StatelessHookState extends HookState> { + @override + R build(HookContext context) { + return hook.build(context); + } +} + +abstract class HookWidget extends StatefulWidget { + const HookWidget({Key key}) : super(key: key); + + @override + HookElement createElement() => HookElement(this); + + @override + _HookWidgetState createState() => _HookWidgetState(); + + @protected + @override + Widget build(covariant HookContext context); +} + +class _HookWidgetState extends State { + @override + void reassemble() { + super.reassemble(); + assert(() { + (context as HookElement)._didReassemble = true; + return true; + }()); + } + + @override + Widget build(covariant HookContext context) { + return widget.build(context); + } +} + +abstract class HookContext extends BuildContext { + R use(Hook hook); + + ValueNotifier useState({T initialData, void dispose(T value)}); + T useMemoized(T valueBuilder(), {List parameters, void dispose(T value)}); + R useValueChanged(T value, R valueChange(T oldValue, R oldResult)); + // void useListenable(Listenable listenable); + void useListenable(Listenable listenable); + T useAnimation(Animation animation); + // T useValueListenable(ValueListenable valueListenable); + // AsyncSnapshot useStream(Stream stream, {T initialData}); + AnimationController useAnimationController({Duration duration}); + TickerProvider useTickerProvider(); +} diff --git a/lib/src/stateful_hook.dart b/lib/src/stateful_hook.dart new file mode 100644 index 00000000..45def290 --- /dev/null +++ b/lib/src/stateful_hook.dart @@ -0,0 +1,297 @@ +// @immutable +// abstract class Hook { +// /// Allows subclasses to have a `const` constructor +// const Hook(); + +// /// Creates the mutable state for this hook linked to its widget creator. +// /// +// /// Subclasses should override this method to return a newly created instance of their associated State subclass: +// /// +// /// ``` +// /// @override +// /// HookState createState() => _MyHookState(); +// /// ``` +// /// +// /// The framework can call this method multiple times over the lifetime of a [HookWidget]. For example, +// /// if the hook is used multiple times, a separate [HookState] must be created for each usage. +// @protected +// HookState> createState(); +// } + +// /// Tracks the lifecycle of [State] objects when asserts are enabled. +// enum _HookLifecycle { +// /// The [State] object has been created. [State.initState] is called at this +// /// time. +// created, + +// /// The [State.initState] method has been called but the [State] object is +// /// not yet ready to build. [State.didChangeDependencies] is called at this time. +// initialized, + +// /// The [State] object is ready to build and [State.dispose] has not yet been +// /// called. +// ready, + +// /// The [State.dispose] method has been called and the [State] object is +// /// no longer able to build. +// defunct, +// } + +// /// The logic and internal state for a [HookWidget] +// /// +// /// A [HookState] +// abstract class HookState> { +// _HookLifecycle _debugLifecycleState = _HookLifecycle.created; + +// /// Equivalent of [State.context] for [HookState] +// @protected +// BuildContext get context => _element; +// Element _element; + +// /// Equivalent of [State.widget] for [HookState] +// T get hook => _hook; +// T _hook; + +// /// Equivalent of [State.initState] for [HookState] +// @protected +// @mustCallSuper +// void initHook() { +// assert(_debugLifecycleState == _HookLifecycle.created); +// } + +// /// Equivalent of [State.deactivate] for [HookState] +// @protected +// @mustCallSuper +// void deactivate() {} + +// /// Equivalent of [State.dispose] for [HookState] +// @protected +// @mustCallSuper +// void dispose() { +// assert(_debugLifecycleState == _HookLifecycle.ready); +// assert(() { +// _debugLifecycleState = _HookLifecycle.defunct; +// return true; +// }()); +// } + +// /// Called everytimes the [HookState] is requested +// /// +// /// [build] is where an [HookState] may use other hooks. This restriction is made to ensure that hooks are unconditionally always requested +// @protected +// R build(HookContext context); + +// /// Equivalent of [State.didUpdateWidget] for [HookState] +// @protected +// @mustCallSuper +// void didUpdateHook(covariant Hook oldHook) {} + +// /// Equivalent of [State.setState] for [HookState] +// @protected +// void setState(VoidCallback fn) { +// assert(fn != null); +// assert(() { +// if (_debugLifecycleState == _HookLifecycle.defunct) { +// throw FlutterError('setState() called after dispose(): $this\n' +// 'This error happens if you call setState() on a HookState object for a widget that ' +// 'no longer appears in the widget tree (e.g., whose parent widget no longer ' +// 'includes the widget in its build). This error can occur when code calls ' +// 'setState() from a timer or an animation callback. The preferred solution is ' +// 'to cancel the timer or stop listening to the animation in the dispose() ' +// 'callback. Another solution is to check the "mounted" property of this ' +// 'object before calling setState() to ensure the object is still in the ' +// 'tree.\n' +// 'This error might indicate a memory leak if setState() is being called ' +// 'because another object is retaining a reference to this State object ' +// 'after it has been removed from the tree. To avoid memory leaks, ' +// 'consider breaking the reference to this object during dispose().'); +// } +// if (_debugLifecycleState == _HookLifecycle.created && _element == null) { +// throw FlutterError('setState() called in constructor: $this\n' +// 'This happens when you call setState() on a HookState object for a widget that ' +// 'hasn\'t been inserted into the widget tree yet. It is not necessary to call ' +// 'setState() in the constructor, since the state is already assumed to be dirty ' +// 'when it is initially created.'); +// } +// return true; +// }()); +// final result = fn() as dynamic; +// assert(() { +// if (result is Future) { +// throw FlutterError('setState() callback argument returned a Future.\n' +// 'The setState() method on $this was called with a closure or method that ' +// 'returned a Future. Maybe it is marked as "async".\n' +// 'Instead of performing asynchronous work inside a call to setState(), first ' +// 'execute the work (without updating the widget state), and then synchronously ' +// 'update the state inside a call to setState().'); +// } +// // We ignore other types of return values so that you can do things like: +// // setState(() => x = 3); +// return true; +// }()); +// _element.markNeedsBuild(); +// } +// } + +// // TODO: take errors from StatefulElement +// class HookElement extends StatelessElement implements HookContext { +// int _hooksIndex; +// List _hooks; + +// bool _debugIsBuilding; + +// HookElement(HookWidget widget) : super(widget); + +// @override +// HookWidget get widget => super.widget as HookWidget; + +// @override +// void performRebuild() { +// _hooksIndex = 0; +// assert(() { +// _debugIsBuilding = true; +// return true; +// }()); +// super.performRebuild(); +// assert(() { +// _debugIsBuilding = false; +// return true; +// }()); +// } + +// @override +// void unmount() { +// super.unmount(); +// if (_hooks != null) { +// for (final hook in _hooks) { +// try { +// hook.dispose(); +// } catch (exception, stack) { +// FlutterError.reportError(FlutterErrorDetails( +// exception: exception, +// stack: stack, +// library: 'hooks library', +// context: 'while disposing $runtimeType', +// )); +// } +// } +// } +// } + +// @override +// R use(Hook hook) { +// assert(_debugIsBuilding == true, ''' +// Hooks should only be called within the build method of a widget. +// Calling them outside of build method leads to an unstable state and is therefore prohibited +// '''); + +// final hooksIndex = _hooksIndex; +// _hooksIndex++; +// _hooks ??= []; + +// HookState> state; +// if (hooksIndex >= _hooks.length) { +// state = hook.createState() +// .._element = this +// .._hook = hook +// ..initHook(); +// _hooks.add(state); +// } else { +// state = _hooks[hooksIndex] as HookState>; +// if (!identical(state._hook, hook)) { +// // TODO: compare type for potential reassemble +// final Hook previousHook = state._hook; +// state +// .._hook = hook +// ..didUpdateHook(previousHook); +// } +// } +// return state.build(this); +// } + +// // AsyncSnapshot useStream(Stream stream, {T initialData}) { +// // return use(_StreamHook(stream: stream, initialData: initialData)); +// // } + +// // @override +// // ValueNotifier useState({T initialData, void dispose(T value)}) { +// // return use(_StateHook(initialData: initialData, dispose: dispose)); +// // } + +// // @override +// // T useAnimation(Animation animation) { +// // return use(_AnimationHook(animation)); +// // } + +// // @override +// // AnimationController useAnimationController({Duration duration}) { +// // return use(_AnimationControllerHook(duration: duration)); +// // } + +// // @override +// // TickerProvider useTickerProvider() { +// // return use(const _TickerProviderHook()); +// // } + +// // @override +// // void useListenable(Listenable listenable) { +// // throw UnimplementedError(); +// // } + +// // @override +// // T useMemoized(T Function() valueBuilder, +// // {List parameters = const [], void dispose(T value)}) { +// // return use(_MemoizedHook( +// // valueBuilder, +// // dispose: dispose, +// // parameters: parameters, +// // )); +// // } + +// // @override +// // R useValueChanged(T value, R valueChange(T oldValue, R oldResult)) { +// // return use(_ValueChangedHook(value, valueChange)); +// // } +// } + +// abstract class StatelessHook extends Hook { +// const StatelessHook(); + +// R build(HookContext context); + +// @override +// _StatelessHookState createState() => _StatelessHookState(); +// } + +// class _StatelessHookState extends HookState> { +// @override +// R build(HookContext context) { +// return hook.build(context); +// } +// } + +// abstract class HookWidget extends StatelessWidget { +// const HookWidget({Key key}) : super(key: key); + +// @override +// HookElement createElement() => HookElement(this); + +// @protected +// @override +// Widget build(covariant HookContext context); +// } + +// abstract class HookContext extends BuildContext { +// R use(Hook hook); + +// // ValueNotifier useState({T initialData, void dispose(T value)}); +// // T useMemoized(T valueBuilder(), {List parameters, void dispose(T value)}); +// // R useValueChanged(T value, R valueChange(T oldValue, R oldResult)); +// // // void useListenable(Listenable listenable); +// // void useListenable(Listenable listenable); +// // T useAnimation(Animation animation); +// // // T useValueListenable(ValueListenable valueListenable); +// // // AsyncSnapshot useStream(Stream stream, {T initialData}); +// // AnimationController useAnimationController({Duration duration}); +// // TickerProvider useTickerProvider(); +// } diff --git a/pubspec.lock b/pubspec.lock index 224c7bb4..ca8ec7c4 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -53,6 +53,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.1.6" + mockito: + dependency: "direct dev" + description: + name: mockito + url: "https://pub.dartlang.org" + source: hosted + version: "4.0.0" path: dependency: transitive description: @@ -60,6 +67,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.6.2" + pedantic: + dependency: "direct dev" + description: + name: pedantic + url: "https://pub.dartlang.org" + source: hosted + version: "1.4.0" quiver: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index eb223bf1..162ba72d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -13,3 +13,5 @@ dependencies: dev_dependencies: flutter_test: sdk: flutter + pedantic: 1.4.0 + mockito: ">=4.0.0 <5.0.0" diff --git a/test/hook_builder_test.dart b/test/hook_builder_test.dart new file mode 100644 index 00000000..414866d2 --- /dev/null +++ b/test/hook_builder_test.dart @@ -0,0 +1,27 @@ +import 'package:flutter/widgets.dart'; +import 'package:flutter_hooks/hook.dart'; +import 'package:mockito/mockito.dart'; + +import 'mock.dart'; + +void main() { + testWidgets('simple build', (tester) async { + final fn = Func1(); + when(fn.call(any)).thenAnswer((_) { + return Container(); + }); + + final createBuilder = () => HookBuilder(builder: fn.call); + final _builder = createBuilder(); + + await tester.pumpWidget(_builder); + + verify(fn.call(any)).called(1); + + await tester.pumpWidget(_builder); + verifyNever(fn.call(any)); + + await tester.pumpWidget(createBuilder()); + verify(fn.call(any)).called(1); + }); +} diff --git a/test/hook_test.dart b/test/hook_test.dart deleted file mode 100644 index 7793154c..00000000 --- a/test/hook_test.dart +++ /dev/null @@ -1,7 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; - -void main() { - test('test', () { - // noop - }); -} diff --git a/test/hook_widget_test.dart b/test/hook_widget_test.dart new file mode 100644 index 00000000..701d126d --- /dev/null +++ b/test/hook_widget_test.dart @@ -0,0 +1,25 @@ +// ignore_for_file: invalid_use_of_protected_member + +void main() { + // testWidgets('do not throw', (tester) { + // tester.pumpWidget(HookBuilder( + // builder: (context) { + // return Container(); + // }, + // )); + // }); + // testWidgets('simple hook', (tester) async { + // final state = _MockHookState(); + // when(state.build(any)).thenReturn(42); + + // int value; + // await tester.pumpWidget(HookBuilder( + // builder: (context) { + // value = context.use(_MockHook(state)); + // return Container(); + // }, + // )); + + // expect(value, equals(42)); + // }); +} diff --git a/test/mock.dart b/test/mock.dart new file mode 100644 index 00000000..740ea573 --- /dev/null +++ b/test/mock.dart @@ -0,0 +1,27 @@ +import 'package:flutter_hooks/hook.dart'; +import 'package:mockito/mockito.dart'; + +export 'package:flutter_test/flutter_test.dart' hide Func0, Func1; + +class MockHook extends Hook { + final MockHookState state; + + MockHook([MockHookState state]) : state = state ?? MockHookState(); + + @override + MockHookState createState() => state; +} + +class MockHookState extends Mock implements HookState> {} + +abstract class _Func0 { + R call(); +} + +class Func0 extends Mock implements _Func0 {} + +abstract class _Func1 { + R call(T1 value); +} + +class Func1 extends Mock implements _Func1 {} From 5b565421a19d3679969e3f6fe460d04fdb9a72c2 Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Mon, 17 Dec 2018 02:31:36 +0100 Subject: [PATCH 002/384] doc --- example/lib/main.dart | 73 +++------ example/lib/main.g.dart | 27 ---- example/pubspec.lock | 310 +------------------------------------ example/pubspec.yaml | 3 - lib/src/hook.dart | 3 - lib/src/hook_impl.dart | 224 ++------------------------- lib/src/hook_widget.dart | 151 ++++++++---------- lib/src/stateful_hook.dart | 297 ----------------------------------- 8 files changed, 101 insertions(+), 987 deletions(-) delete mode 100644 example/lib/main.g.dart delete mode 100644 lib/src/stateful_hook.dart diff --git a/example/lib/main.dart b/example/lib/main.dart index ef6bf8fd..03aaa6b9 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -1,70 +1,35 @@ import 'package:flutter/material.dart'; import 'package:flutter_hooks/hook.dart'; -import 'package:functional_widget_annotation/functional_widget_annotation.dart'; -part 'main.g.dart'; +void main() => runApp(_MyApp()); -void main() => runApp(MyApp()); - -class MyApp extends StatelessWidget { - // This widget is the root of your application. +class _MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return const MaterialApp( title: 'Flutter Demo', - home: _Foo(), + home: _Counter(), ); } } -@widget -Widget _testAnimation(HookContext context, {Color color}) { - final controller = - context.useAnimationController(duration: const Duration(seconds: 5)); - - final colorTween = context.useValueChanged( - color, - (Color oldValue, Animation oldResult) { - return ColorTween( - begin: oldResult?.value ?? oldValue, - end: color, - ).animate(controller..forward(from: 0)); - }, - ) ?? - AlwaysStoppedAnimation(color); +class _Counter extends HookWidget { + const _Counter({Key key}) : super(key: key); - final currentColor = context.useAnimation(colorTween); - return Container(color: currentColor); -} - -@widget -Widget _foo(HookContext context) { - final toggle = context.useState(initialData: false); - final counter = context.useState(initialData: 0); + @override + Widget build(HookContext context) { + final counter = context.useState(initialData: 0); - return Scaffold( - body: GestureDetector( - onTap: () { - toggle.value = !toggle.value; - }, - child: Stack( - children: [ - Positioned.fill( - child: _TestAnimation( - color: toggle.value - ? const Color.fromARGB(255, 255, 0, 0) - : const Color.fromARGB(255, 0, 0, 255), - ), - ), - Center( - child: Text(counter.value.toString()), - ) - ], + return Scaffold( + appBar: AppBar( + title: const Text('Counter app'), ), - ), - floatingActionButton: FloatingActionButton( - onPressed: () => counter.value++, - child: const Icon(Icons.plus_one), - ), - ); + body: Center( + child: Text(counter.value.toString()), + ), + floatingActionButton: FloatingActionButton( + onPressed: () => counter.value++, + ), + ); + } } diff --git a/example/lib/main.g.dart b/example/lib/main.g.dart deleted file mode 100644 index b65c6ace..00000000 --- a/example/lib/main.g.dart +++ /dev/null @@ -1,27 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'main.dart'; - -// ************************************************************************** -// Generator: FunctionalWidget -// ************************************************************************** - -class _TestAnimation extends HookWidget { - const _TestAnimation({Key key, this.color}) : super(key: key); - - final Color color; - - @override - Widget build(HookContext _context) { - return _testAnimation(_context, color: color); - } -} - -class _Foo extends HookWidget { - const _Foo({Key key}) : super(key: key); - - @override - Widget build(HookContext _context) { - return _foo(_context); - } -} diff --git a/example/pubspec.lock b/example/pubspec.lock index 291cee91..d6580f15 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -1,20 +1,6 @@ # Generated by pub # See https://www.dartlang.org/tools/pub/glossary#lockfile packages: - analyzer: - dependency: transitive - description: - name: analyzer - url: "https://pub.dartlang.org" - source: hosted - version: "0.33.6" - args: - dependency: transitive - description: - name: args - url: "https://pub.dartlang.org" - source: hosted - version: "1.5.1" async: dependency: transitive description: @@ -29,55 +15,6 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.0.4" - build: - dependency: transitive - description: - name: build - url: "https://pub.dartlang.org" - source: hosted - version: "1.0.1" - build_config: - dependency: transitive - description: - name: build_config - url: "https://pub.dartlang.org" - source: hosted - version: "0.3.1+4" - build_resolvers: - dependency: transitive - description: - name: build_resolvers - url: "https://pub.dartlang.org" - source: hosted - version: "0.2.2+6" - build_runner: - dependency: "direct dev" - description: - name: build_runner - url: "https://pub.dartlang.org" - source: hosted - version: "1.1.2" - build_runner_core: - dependency: transitive - description: - name: build_runner_core - url: "https://pub.dartlang.org" - source: hosted - version: "1.1.1" - built_collection: - dependency: transitive - description: - name: built_collection - url: "https://pub.dartlang.org" - source: hosted - version: "4.1.0" - built_value: - dependency: transitive - description: - name: built_value - url: "https://pub.dartlang.org" - source: hosted - version: "6.1.6" charcode: dependency: transitive description: @@ -85,13 +22,6 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.1.2" - code_builder: - dependency: transitive - description: - name: code_builder - url: "https://pub.dartlang.org" - source: hosted - version: "3.1.3" collection: dependency: transitive description: @@ -99,41 +29,6 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.14.11" - convert: - dependency: transitive - description: - name: convert - url: "https://pub.dartlang.org" - source: hosted - version: "2.0.2" - crypto: - dependency: transitive - description: - name: crypto - url: "https://pub.dartlang.org" - source: hosted - version: "2.0.6" - csslib: - dependency: transitive - description: - name: csslib - url: "https://pub.dartlang.org" - source: hosted - version: "0.14.6" - dart_style: - dependency: transitive - description: - name: dart_style - url: "https://pub.dartlang.org" - source: hosted - version: "1.2.0" - fixnum: - dependency: transitive - description: - name: fixnum - url: "https://pub.dartlang.org" - source: hosted - version: "0.10.9" flutter: dependency: "direct main" description: flutter @@ -151,97 +46,6 @@ packages: description: flutter source: sdk version: "0.0.0" - front_end: - dependency: transitive - description: - name: front_end - url: "https://pub.dartlang.org" - source: hosted - version: "0.1.6+8" - functional_widget: - dependency: "direct dev" - description: - name: functional_widget - url: "https://pub.dartlang.org" - source: hosted - version: "0.2.1" - functional_widget_annotation: - dependency: "direct main" - description: - name: functional_widget_annotation - url: "https://pub.dartlang.org" - source: hosted - version: "0.1.0" - glob: - dependency: transitive - description: - name: glob - url: "https://pub.dartlang.org" - source: hosted - version: "1.1.7" - graphs: - dependency: transitive - description: - name: graphs - url: "https://pub.dartlang.org" - source: hosted - version: "0.1.3+1" - html: - dependency: transitive - description: - name: html - url: "https://pub.dartlang.org" - source: hosted - version: "0.13.3+3" - http_multi_server: - dependency: transitive - description: - name: http_multi_server - url: "https://pub.dartlang.org" - source: hosted - version: "2.0.5" - http_parser: - dependency: transitive - description: - name: http_parser - url: "https://pub.dartlang.org" - source: hosted - version: "3.1.3" - io: - dependency: transitive - description: - name: io - url: "https://pub.dartlang.org" - source: hosted - version: "0.3.3" - js: - dependency: transitive - description: - name: js - url: "https://pub.dartlang.org" - source: hosted - version: "0.6.1+1" - json_annotation: - dependency: transitive - description: - name: json_annotation - url: "https://pub.dartlang.org" - source: hosted - version: "2.0.0" - kernel: - dependency: transitive - description: - name: kernel - url: "https://pub.dartlang.org" - source: hosted - version: "0.3.6+8" - logging: - dependency: transitive - description: - name: logging - url: "https://pub.dartlang.org" - source: hosted - version: "0.11.3+2" matcher: dependency: transitive description: @@ -256,20 +60,6 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.1.6" - mime: - dependency: transitive - description: - name: mime - url: "https://pub.dartlang.org" - source: hosted - version: "0.9.6+2" - package_config: - dependency: transitive - description: - name: package_config - url: "https://pub.dartlang.org" - source: hosted - version: "1.0.5" path: dependency: transitive description: @@ -277,41 +67,6 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.6.2" - pedantic: - dependency: transitive - description: - name: pedantic - url: "https://pub.dartlang.org" - source: hosted - version: "1.3.0" - plugin: - dependency: transitive - description: - name: plugin - url: "https://pub.dartlang.org" - source: hosted - version: "0.2.0+3" - pool: - dependency: transitive - description: - name: pool - url: "https://pub.dartlang.org" - source: hosted - version: "1.3.6" - pub_semver: - dependency: transitive - description: - name: pub_semver - url: "https://pub.dartlang.org" - source: hosted - version: "1.4.2" - pubspec_parse: - dependency: transitive - description: - name: pubspec_parse - url: "https://pub.dartlang.org" - source: hosted - version: "0.1.2+3" quiver: dependency: transitive description: @@ -319,32 +74,11 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.0.1" - shelf: - dependency: transitive - description: - name: shelf - url: "https://pub.dartlang.org" - source: hosted - version: "0.7.3+3" - shelf_web_socket: - dependency: transitive - description: - name: shelf_web_socket - url: "https://pub.dartlang.org" - source: hosted - version: "0.2.2+4" sky_engine: dependency: transitive description: flutter source: sdk version: "0.0.99" - source_gen: - dependency: transitive - description: - name: source_gen - url: "https://pub.dartlang.org" - source: hosted - version: "0.9.1+3" source_span: dependency: transitive description: @@ -366,13 +100,6 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.6.8" - stream_transform: - dependency: transitive - description: - name: stream_transform - url: "https://pub.dartlang.org" - source: hosted - version: "0.0.14+1" string_scanner: dependency: transitive description: @@ -394,13 +121,6 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.2.1" - timing: - dependency: transitive - description: - name: timing - url: "https://pub.dartlang.org" - source: hosted - version: "0.1.1+1" typed_data: dependency: transitive description: @@ -408,13 +128,6 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.1.6" - utf: - dependency: transitive - description: - name: utf - url: "https://pub.dartlang.org" - source: hosted - version: "0.9.0+5" vector_math: dependency: transitive description: @@ -422,26 +135,5 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.0.8" - watcher: - dependency: transitive - description: - name: watcher - url: "https://pub.dartlang.org" - source: hosted - version: "0.9.7+10" - web_socket_channel: - dependency: transitive - description: - name: web_socket_channel - url: "https://pub.dartlang.org" - source: hosted - version: "1.0.9" - yaml: - dependency: transitive - description: - name: yaml - url: "https://pub.dartlang.org" - source: hosted - version: "2.1.15" sdks: - dart: ">=2.1.0-dev.5.0 <3.0.0" + dart: ">=2.0.0 <3.0.0" diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 7ad07be4..2feea3d3 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -10,13 +10,10 @@ dependencies: flutter: sdk: flutter flutter_hooks: 0.0.1 - functional_widget_annotation: 0.1.0 dev_dependencies: flutter_test: sdk: flutter - functional_widget: 0.2.1 - build_runner: dependency_overrides: flutter_hooks: diff --git a/lib/src/hook.dart b/lib/src/hook.dart index e27e05b0..bd681bd0 100644 --- a/lib/src/hook.dart +++ b/lib/src/hook.dart @@ -1,8 +1,5 @@ -import 'dart:async'; - import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/scheduler.dart' show Ticker, TickerCallback; import 'package:flutter/widgets.dart'; part 'hook_widget.dart'; diff --git a/lib/src/hook_impl.dart b/lib/src/hook_impl.dart index 7d1a75f8..fd76c580 100644 --- a/lib/src/hook_impl.dart +++ b/lib/src/hook_impl.dart @@ -1,162 +1,10 @@ part of 'hook.dart'; -class _StreamHook extends Hook> { - final Stream stream; - final T initialData; - _StreamHook({this.stream, this.initialData}); - - @override - _StreamHookState createState() => _StreamHookState(); -} - -class _StreamHookState extends HookState, _StreamHook> { - StreamSubscription subscription; - AsyncSnapshot snapshot; - @override - void initHook() { - super.initHook(); - _listen(hook.stream); - } - - @override - void didUpdateHook(_StreamHook oldHook) { - super.didUpdateHook(oldHook); - if (oldHook.stream != hook.stream) { - _listen(hook.stream); - } - } - - void _listen(Stream stream) { - subscription?.cancel(); - snapshot = stream == null - ? AsyncSnapshot.nothing() - : AsyncSnapshot.withData(ConnectionState.waiting, hook.initialData); - subscription = - hook.stream.listen(_onData, onDone: _onDone, onError: _onError); - } - - void _onData(T event) { - setState(() { - snapshot = AsyncSnapshot.withData(ConnectionState.active, event); - }); - } - - void _onDone() { - setState(() { - snapshot = snapshot.hasError - ? AsyncSnapshot.withError(ConnectionState.active, snapshot.error) - : AsyncSnapshot.withData(ConnectionState.done, snapshot.data); - }); - } - - void _onError(Object error) { - setState(() { - snapshot = AsyncSnapshot.withError(ConnectionState.active, error); - }); - } - - @override - void dispose() { - subscription?.cancel(); - super.dispose(); - } - - @override - AsyncSnapshot build(HookContext context) { - return snapshot; - } -} - -class _TickerProviderHook extends Hook { - const _TickerProviderHook(); - - @override - _TickerProviderHookState createState() => _TickerProviderHookState(); -} - -class _TickerProviderHookState - extends HookState - implements TickerProvider { - Ticker _ticker; - - @override - Ticker createTicker(TickerCallback onTick) { - assert(() { - if (_ticker == null) return true; - // TODO: update error - throw FlutterError( - '$runtimeType is a SingleTickerProviderStateMixin but multiple tickers were created.\n' - 'A SingleTickerProviderStateMixin can only be used as a TickerProvider once. If a ' - 'State is used for multiple AnimationController objects, or if it is passed to other ' - 'objects and those objects might use it more than one time in total, then instead of ' - 'mixing in a SingleTickerProviderStateMixin, use a regular TickerProviderStateMixin.'); - }()); - _ticker = Ticker(onTick, debugLabel: 'created by $this'); - // TODO: check if the following is still valid - // We assume that this is called from initState, build, or some sort of - // event handler, and that thus TickerMode.of(context) would return true. We - // can't actually check that here because if we're in initState then we're - // not allowed to do inheritance checks yet. - return _ticker; - } - - @override - void dispose() { - assert(() { - if (_ticker == null || !_ticker.isActive) return true; - // TODO: update error - throw FlutterError('$this was disposed with an active Ticker.\n' - '$runtimeType created a Ticker via its SingleTickerProviderStateMixin, but at the time ' - 'dispose() was called on the mixin, that Ticker was still active. The Ticker must ' - 'be disposed before calling super.dispose(). Tickers used by AnimationControllers ' - 'should be disposed by calling dispose() on the AnimationController itself. ' - 'Otherwise, the ticker will leak.\n' - 'The offending ticker was: ${_ticker.toString(debugIncludeStack: true)}'); - }()); - super.dispose(); - } - - @override - TickerProvider build(HookContext context) { - if (_ticker != null) _ticker.muted = !TickerMode.of(context); - - return this; - } -} - -class _AnimationControllerHook extends StatelessHook { - final Duration duration; - - const _AnimationControllerHook({this.duration}); - - @override - AnimationController build(HookContext context) { - final tickerProvider = context.useTickerProvider(); - - final animationController = context.useMemoized( - () => AnimationController(vsync: tickerProvider, duration: duration), - dispose: (animationController) => animationController.dispose(), - ); - - context - ..useValueChanged(tickerProvider, (_, __) { - animationController.resync(tickerProvider); - }) - ..useValueChanged(duration, (_, __) { - animationController.duration = duration; - }); - - return animationController; - } -} - class _MemoizedHook extends Hook { - final T Function() valueBuilder; - final void Function(T value) dispose; + final T Function(T oldValue) valueBuilder; final List parameters; - const _MemoizedHook(this.valueBuilder, - {this.parameters = const [], this.dispose}) + const _MemoizedHook(this.valueBuilder, {this.parameters = const []}) : assert(valueBuilder != null), assert(parameters != null); @@ -170,7 +18,7 @@ class _MemoizedHookState extends HookState> { @override void initHook() { super.initHook(); - value = hook.valueBuilder(); + value = hook.valueBuilder(null); } @override @@ -179,21 +27,10 @@ class _MemoizedHookState extends HookState> { if (hook.parameters != oldHook.parameters && (hook.parameters.length != oldHook.parameters.length || _hasDiffWith(oldHook.parameters))) { - if (hook.dispose != null) { - hook.dispose(value); - } - value = hook.valueBuilder(); + value = hook.valueBuilder(value); } } - @override - void dispose() { - if (hook.dispose != null) { - hook.dispose(value); - } - super.dispose(); - } - bool _hasDiffWith(List parameters) { for (var i = 0; i < parameters.length; i++) { if (parameters[i] != hook.parameters[i]) { @@ -238,56 +75,16 @@ class _ValueChangedHookState } } -class _AnimationHook extends Hook { - final Animation animation; - - const _AnimationHook(this.animation) : assert(animation != null); - - @override - _AnimationHookState createState() => _AnimationHookState(); -} - -class _AnimationHookState extends HookState> { - @override - void initHook() { - super.initHook(); - hook.animation.addListener(_listener); - } - - @override - void dispose() { - hook.animation.removeListener(_listener); - super.dispose(); - } - - @override - T build(HookContext context) { - context.useValueChanged(hook.animation, valueChange); - return hook.animation.value; - } - - void _listener() { - setState(() {}); - } - - void valueChange(Animation previous, _) { - previous.removeListener(_listener); - hook.animation.addListener(_listener); - } -} - class _StateHook extends Hook> { final T initialData; - final void Function(T value) dispose; - const _StateHook({this.initialData, this.dispose}); + const _StateHook({this.initialData}); @override _StateHookState createState() => _StateHookState(); } class _StateHookState extends HookState, _StateHook> { - TickerProvider _ticker; ValueNotifier _state; @override @@ -298,9 +95,6 @@ class _StateHookState extends HookState, _StateHook> { @override void dispose() { - if (hook.dispose != null) { - hook.dispose(_state.value); - } _state.dispose(); super.dispose(); } @@ -315,9 +109,17 @@ class _StateHookState extends HookState, _StateHook> { } } +/// A [HookWidget] that defer its [HookWidget.build] to a callback class HookBuilder extends HookWidget { + /// The callback used by [HookBuilder] to create a widget. + /// + /// If the passed [HookContext] trigger a rebuild, [builder] will be called again. + /// [builder] must not return `null`. final Widget Function(HookContext context) builder; + /// Creates a widget that delegates its build to a callback. + /// + /// The [builder] argument must not be null. const HookBuilder({ @required this.builder, Key key, diff --git a/lib/src/hook_widget.dart b/lib/src/hook_widget.dart index fab16865..cb194db1 100644 --- a/lib/src/hook_widget.dart +++ b/lib/src/hook_widget.dart @@ -89,8 +89,7 @@ part of 'hook.dart'; /// This is undesired because every single widget that want to use an [AnimationController] will have to /// rewrite this extact piece of code. /// -/// With hooks it is possible to extract that exact piece of code into a reusable one. In fact, one is already provided by default: -/// [HookContext.useAnimationController] +/// With hooks it is possible to extract that exact piece of code into a reusable one. /// /// This means that with [HookWidget] the following code is equivalent to the previous example: /// @@ -130,25 +129,6 @@ abstract class Hook { HookState> createState(); } -/// Tracks the lifecycle of [State] objects when asserts are enabled. -enum _HookLifecycle { - /// The [State] object has been created. [State.initState] is called at this - /// time. - created, - - /// The [State.initState] method has been called but the [State] object is - /// not yet ready to build. [State.didChangeDependencies] is called at this time. - initialized, - - /// The [State] object is ready to build and [State.dispose] has not yet been - /// called. - ready, - - /// The [State.dispose] method has been called and the [State] object is - /// no longer able to build. - defunct, -} - /// The logic and internal state for a [HookWidget] /// /// A [HookState] @@ -183,20 +163,22 @@ abstract class HookState> { /// Equivalent of [State.setState] for [HookState] @protected void setState(VoidCallback fn) { - // ignore: invalid_use_of_protected_member + // ignore: invalid_use_of_protected_member _element.setState(fn); } } +/// An [Element] that uses a [HookWidget] as its configuration. class HookElement extends StatefulElement implements HookContext { Iterator _currentHook; - int _hooksIndex; + int _debugHooksIndex; List _hooks; bool _debugIsBuilding; bool _didReassemble; bool _isFirstBuild; + /// Creates an element that uses the given widget as its configuration. HookElement(HookWidget widget) : super(widget); @override @@ -207,8 +189,8 @@ class HookElement extends StatefulElement implements HookContext { _currentHook = _hooks?.iterator; // first iterator always has null for unknown reasons _currentHook?.moveNext(); - _hooksIndex = 0; assert(() { + _debugHooksIndex = 0; _isFirstBuild ??= true; _didReassemble ??= false; _debugIsBuilding = true; @@ -265,7 +247,7 @@ class HookElement extends StatefulElement implements HookContext { if (_currentHook.current?.hook?.runtimeType == hook.runtimeType) { return true; } else if (_currentHook.current != null) { - for (var i = _hooks.length - 1; i >= _hooksIndex; i--) { + for (var i = _hooks.length - 1; i >= _debugHooksIndex; i--) { _hooks.removeLast().dispose(); } } @@ -284,15 +266,16 @@ class HookElement extends StatefulElement implements HookContext { _currentHook.moveNext(); if (hookState._hook != hook) { - // TODO: compare type for potential reassemble final Hook previousHook = hookState._hook; hookState .._hook = hook ..didUpdateHook(previousHook); } } - - _hooksIndex++; + assert(() { + _debugHooksIndex++; + return true; + }()); return hookState.build(this); } @@ -303,41 +286,16 @@ class HookElement extends StatefulElement implements HookContext { ..initHook(); } - AsyncSnapshot useStream(Stream stream, {T initialData}) { - return use(_StreamHook(stream: stream, initialData: initialData)); - } - @override - ValueNotifier useState({T initialData, void dispose(T value)}) { - return use(_StateHook(initialData: initialData, dispose: dispose)); + ValueNotifier useState({T initialData}) { + return use(_StateHook(initialData: initialData)); } @override - T useAnimation(Animation animation) { - return use(_AnimationHook(animation)); - } - - @override - AnimationController useAnimationController({Duration duration}) { - return use(_AnimationControllerHook(duration: duration)); - } - - @override - TickerProvider useTickerProvider() { - return use(const _TickerProviderHook()); - } - - @override - void useListenable(Listenable listenable) { - throw UnimplementedError(); - } - - @override - T useMemoized(T Function() valueBuilder, - {List parameters = const [], void dispose(T value)}) { + T useMemoized(T Function(T oldValue) valueBuilder, + {List parameters = const []}) { return use(_MemoizedHook( valueBuilder, - dispose: dispose, parameters: parameters, )); } @@ -348,23 +306,16 @@ class HookElement extends StatefulElement implements HookContext { } } -abstract class StatelessHook extends Hook { - const StatelessHook(); - - R build(HookContext context); - - @override - _StatelessHookState createState() => _StatelessHookState(); -} - -class _StatelessHookState extends HookState> { - @override - R build(HookContext context) { - return hook.build(context); - } -} - +/// A [Widget] that can use [Hook] +/// +/// It's usage is very similar to [StatelessWidget]: +/// [HookWidget] do not have any life-cycle and implements +/// only a [build] method. +/// +/// The difference is that it can use [Hook], which allows +/// [HookWidget] to store mutable data without implementing a [State]. abstract class HookWidget extends StatefulWidget { + /// Initializes [key] for subclasses. const HookWidget({Key key}) : super(key: key); @override @@ -373,8 +324,12 @@ abstract class HookWidget extends StatefulWidget { @override _HookWidgetState createState() => _HookWidgetState(); + /// Describes the part of the user interface represented by this widget. + /// + /// See also: + /// + /// * [StatelessWidget.build] @protected - @override Widget build(covariant HookContext context); } @@ -394,17 +349,47 @@ class _HookWidgetState extends State { } } +/// A [BuildContext] that can use a [Hook]. +/// +/// See also: +/// +/// * [BuildContext] abstract class HookContext extends BuildContext { + /// Register a [Hook] and returns its value + /// + /// [use] must be called withing [HookWidget.build] and + /// all calls to [use] must be made unconditionally, always + /// on the same order. + /// + /// See [Hook] for more explanations. R use(Hook hook); - ValueNotifier useState({T initialData, void dispose(T value)}); - T useMemoized(T valueBuilder(), {List parameters, void dispose(T value)}); + /// Create a mutable value and subscribes to it. + /// + /// Whenever [ValueNotifier.value] updates, it will mark the caller [HookContext] + /// as needing build. + /// On first call, inits [ValueNotifier] to [initialData]. [initialData] is ignored + /// on subsequent calls. + /// + /// See also: + /// + /// * [use] + /// * [Hook] + ValueNotifier useState({T initialData}); + + /// Create and cache the instance of an object. + /// + /// [useMemoized] will immediatly call [valueBuilder] on first call and store its result. + /// Later calls to [useMemoized] will reuse the created instance. + /// + /// * [parameters] can be use to specify a list of objects for [useMemoized] to watch. + /// So that whenever [operator==] fails on any parameter or if the length of [parameters] changes, + /// [valueBuilder] is called again. + T useMemoized(T valueBuilder(T previousValue), {List parameters}); + + /// Watches a value. + /// + /// Whenever [useValueChanged] is called with a diffent [value], calls [valueChange]. + /// The value returned by [useValueChanged] is the latest returned value of [valueChange] or `null`. R useValueChanged(T value, R valueChange(T oldValue, R oldResult)); - // void useListenable(Listenable listenable); - void useListenable(Listenable listenable); - T useAnimation(Animation animation); - // T useValueListenable(ValueListenable valueListenable); - // AsyncSnapshot useStream(Stream stream, {T initialData}); - AnimationController useAnimationController({Duration duration}); - TickerProvider useTickerProvider(); } diff --git a/lib/src/stateful_hook.dart b/lib/src/stateful_hook.dart deleted file mode 100644 index 45def290..00000000 --- a/lib/src/stateful_hook.dart +++ /dev/null @@ -1,297 +0,0 @@ -// @immutable -// abstract class Hook { -// /// Allows subclasses to have a `const` constructor -// const Hook(); - -// /// Creates the mutable state for this hook linked to its widget creator. -// /// -// /// Subclasses should override this method to return a newly created instance of their associated State subclass: -// /// -// /// ``` -// /// @override -// /// HookState createState() => _MyHookState(); -// /// ``` -// /// -// /// The framework can call this method multiple times over the lifetime of a [HookWidget]. For example, -// /// if the hook is used multiple times, a separate [HookState] must be created for each usage. -// @protected -// HookState> createState(); -// } - -// /// Tracks the lifecycle of [State] objects when asserts are enabled. -// enum _HookLifecycle { -// /// The [State] object has been created. [State.initState] is called at this -// /// time. -// created, - -// /// The [State.initState] method has been called but the [State] object is -// /// not yet ready to build. [State.didChangeDependencies] is called at this time. -// initialized, - -// /// The [State] object is ready to build and [State.dispose] has not yet been -// /// called. -// ready, - -// /// The [State.dispose] method has been called and the [State] object is -// /// no longer able to build. -// defunct, -// } - -// /// The logic and internal state for a [HookWidget] -// /// -// /// A [HookState] -// abstract class HookState> { -// _HookLifecycle _debugLifecycleState = _HookLifecycle.created; - -// /// Equivalent of [State.context] for [HookState] -// @protected -// BuildContext get context => _element; -// Element _element; - -// /// Equivalent of [State.widget] for [HookState] -// T get hook => _hook; -// T _hook; - -// /// Equivalent of [State.initState] for [HookState] -// @protected -// @mustCallSuper -// void initHook() { -// assert(_debugLifecycleState == _HookLifecycle.created); -// } - -// /// Equivalent of [State.deactivate] for [HookState] -// @protected -// @mustCallSuper -// void deactivate() {} - -// /// Equivalent of [State.dispose] for [HookState] -// @protected -// @mustCallSuper -// void dispose() { -// assert(_debugLifecycleState == _HookLifecycle.ready); -// assert(() { -// _debugLifecycleState = _HookLifecycle.defunct; -// return true; -// }()); -// } - -// /// Called everytimes the [HookState] is requested -// /// -// /// [build] is where an [HookState] may use other hooks. This restriction is made to ensure that hooks are unconditionally always requested -// @protected -// R build(HookContext context); - -// /// Equivalent of [State.didUpdateWidget] for [HookState] -// @protected -// @mustCallSuper -// void didUpdateHook(covariant Hook oldHook) {} - -// /// Equivalent of [State.setState] for [HookState] -// @protected -// void setState(VoidCallback fn) { -// assert(fn != null); -// assert(() { -// if (_debugLifecycleState == _HookLifecycle.defunct) { -// throw FlutterError('setState() called after dispose(): $this\n' -// 'This error happens if you call setState() on a HookState object for a widget that ' -// 'no longer appears in the widget tree (e.g., whose parent widget no longer ' -// 'includes the widget in its build). This error can occur when code calls ' -// 'setState() from a timer or an animation callback. The preferred solution is ' -// 'to cancel the timer or stop listening to the animation in the dispose() ' -// 'callback. Another solution is to check the "mounted" property of this ' -// 'object before calling setState() to ensure the object is still in the ' -// 'tree.\n' -// 'This error might indicate a memory leak if setState() is being called ' -// 'because another object is retaining a reference to this State object ' -// 'after it has been removed from the tree. To avoid memory leaks, ' -// 'consider breaking the reference to this object during dispose().'); -// } -// if (_debugLifecycleState == _HookLifecycle.created && _element == null) { -// throw FlutterError('setState() called in constructor: $this\n' -// 'This happens when you call setState() on a HookState object for a widget that ' -// 'hasn\'t been inserted into the widget tree yet. It is not necessary to call ' -// 'setState() in the constructor, since the state is already assumed to be dirty ' -// 'when it is initially created.'); -// } -// return true; -// }()); -// final result = fn() as dynamic; -// assert(() { -// if (result is Future) { -// throw FlutterError('setState() callback argument returned a Future.\n' -// 'The setState() method on $this was called with a closure or method that ' -// 'returned a Future. Maybe it is marked as "async".\n' -// 'Instead of performing asynchronous work inside a call to setState(), first ' -// 'execute the work (without updating the widget state), and then synchronously ' -// 'update the state inside a call to setState().'); -// } -// // We ignore other types of return values so that you can do things like: -// // setState(() => x = 3); -// return true; -// }()); -// _element.markNeedsBuild(); -// } -// } - -// // TODO: take errors from StatefulElement -// class HookElement extends StatelessElement implements HookContext { -// int _hooksIndex; -// List _hooks; - -// bool _debugIsBuilding; - -// HookElement(HookWidget widget) : super(widget); - -// @override -// HookWidget get widget => super.widget as HookWidget; - -// @override -// void performRebuild() { -// _hooksIndex = 0; -// assert(() { -// _debugIsBuilding = true; -// return true; -// }()); -// super.performRebuild(); -// assert(() { -// _debugIsBuilding = false; -// return true; -// }()); -// } - -// @override -// void unmount() { -// super.unmount(); -// if (_hooks != null) { -// for (final hook in _hooks) { -// try { -// hook.dispose(); -// } catch (exception, stack) { -// FlutterError.reportError(FlutterErrorDetails( -// exception: exception, -// stack: stack, -// library: 'hooks library', -// context: 'while disposing $runtimeType', -// )); -// } -// } -// } -// } - -// @override -// R use(Hook hook) { -// assert(_debugIsBuilding == true, ''' -// Hooks should only be called within the build method of a widget. -// Calling them outside of build method leads to an unstable state and is therefore prohibited -// '''); - -// final hooksIndex = _hooksIndex; -// _hooksIndex++; -// _hooks ??= []; - -// HookState> state; -// if (hooksIndex >= _hooks.length) { -// state = hook.createState() -// .._element = this -// .._hook = hook -// ..initHook(); -// _hooks.add(state); -// } else { -// state = _hooks[hooksIndex] as HookState>; -// if (!identical(state._hook, hook)) { -// // TODO: compare type for potential reassemble -// final Hook previousHook = state._hook; -// state -// .._hook = hook -// ..didUpdateHook(previousHook); -// } -// } -// return state.build(this); -// } - -// // AsyncSnapshot useStream(Stream stream, {T initialData}) { -// // return use(_StreamHook(stream: stream, initialData: initialData)); -// // } - -// // @override -// // ValueNotifier useState({T initialData, void dispose(T value)}) { -// // return use(_StateHook(initialData: initialData, dispose: dispose)); -// // } - -// // @override -// // T useAnimation(Animation animation) { -// // return use(_AnimationHook(animation)); -// // } - -// // @override -// // AnimationController useAnimationController({Duration duration}) { -// // return use(_AnimationControllerHook(duration: duration)); -// // } - -// // @override -// // TickerProvider useTickerProvider() { -// // return use(const _TickerProviderHook()); -// // } - -// // @override -// // void useListenable(Listenable listenable) { -// // throw UnimplementedError(); -// // } - -// // @override -// // T useMemoized(T Function() valueBuilder, -// // {List parameters = const [], void dispose(T value)}) { -// // return use(_MemoizedHook( -// // valueBuilder, -// // dispose: dispose, -// // parameters: parameters, -// // )); -// // } - -// // @override -// // R useValueChanged(T value, R valueChange(T oldValue, R oldResult)) { -// // return use(_ValueChangedHook(value, valueChange)); -// // } -// } - -// abstract class StatelessHook extends Hook { -// const StatelessHook(); - -// R build(HookContext context); - -// @override -// _StatelessHookState createState() => _StatelessHookState(); -// } - -// class _StatelessHookState extends HookState> { -// @override -// R build(HookContext context) { -// return hook.build(context); -// } -// } - -// abstract class HookWidget extends StatelessWidget { -// const HookWidget({Key key}) : super(key: key); - -// @override -// HookElement createElement() => HookElement(this); - -// @protected -// @override -// Widget build(covariant HookContext context); -// } - -// abstract class HookContext extends BuildContext { -// R use(Hook hook); - -// // ValueNotifier useState({T initialData, void dispose(T value)}); -// // T useMemoized(T valueBuilder(), {List parameters, void dispose(T value)}); -// // R useValueChanged(T value, R valueChange(T oldValue, R oldResult)); -// // // void useListenable(Listenable listenable); -// // void useListenable(Listenable listenable); -// // T useAnimation(Animation animation); -// // // T useValueListenable(ValueListenable valueListenable); -// // // AsyncSnapshot useStream(Stream stream, {T initialData}); -// // AnimationController useAnimationController({Duration duration}); -// // TickerProvider useTickerProvider(); -// } From fd86e8f70bee29b5bcdee28125fcef71eaf14433 Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Mon, 17 Dec 2018 03:58:37 +0100 Subject: [PATCH 003/384] some test --- coverage/lcov.info | 148 ++++++++++++++++++++++++++++++++++++ lib/src/hook_widget.dart | 4 +- test/hook_builder_test.dart | 8 ++ test/hook_widget_test.dart | 99 +++++++++++++++++++----- test/mock.dart | 52 +++++++++++++ 5 files changed, 287 insertions(+), 24 deletions(-) create mode 100644 coverage/lcov.info diff --git a/coverage/lcov.info b/coverage/lcov.info new file mode 100644 index 00000000..42891949 --- /dev/null +++ b/coverage/lcov.info @@ -0,0 +1,148 @@ +SF:lib/src/hook_impl.dart +DA:7,0 +DA:8,0 +DA:9,0 +DA:11,0 +DA:12,0 +DA:18,0 +DA:20,0 +DA:21,0 +DA:24,0 +DA:26,0 +DA:27,0 +DA:28,0 +DA:29,0 +DA:30,0 +DA:34,0 +DA:35,0 +DA:36,0 +DA:43,0 +DA:45,0 +DA:53,0 +DA:54,0 +DA:56,0 +DA:57,0 +DA:64,0 +DA:66,0 +DA:67,0 +DA:68,0 +DA:72,0 +DA:74,0 +DA:81,0 +DA:83,0 +DA:84,0 +DA:90,0 +DA:92,0 +DA:93,0 +DA:96,0 +DA:98,0 +DA:99,0 +DA:102,0 +DA:104,0 +DA:107,0 +DA:108,0 +DA:123,2 +DA:126,1 +DA:127,2 +DA:129,2 +DA:130,2 +LF:47 +LH:5 +end_of_record +SF:lib/src/hook_widget.dart +DA:115,1 +DA:135,0 +DA:136,0 +DA:140,2 +DA:144,1 +DA:145,0 +DA:148,1 +DA:149,0 +DA:154,0 +DA:155,0 +DA:158,1 +DA:159,0 +DA:162,0 +DA:163,0 +DA:165,0 +DA:180,4 +DA:182,2 +DA:183,2 +DA:185,2 +DA:187,5 +DA:189,3 +DA:190,2 +DA:191,2 +DA:192,2 +DA:193,2 +DA:194,2 +DA:196,2 +DA:197,2 +DA:198,2 +DA:199,2 +DA:200,2 +DA:201,2 +DA:203,2 +DA:206,2 +DA:208,2 +DA:209,2 +DA:210,2 +DA:212,1 +DA:214,0 +DA:218,0 +DA:225,1 +DA:227,2 +DA:234,1 +DA:235,2 +DA:236,1 +DA:237,2 +DA:238,2 +DA:241,1 +DA:242,1 +DA:245,0 +DA:247,0 +DA:248,0 +DA:249,0 +DA:252,0 +DA:253,0 +DA:254,0 +DA:255,0 +DA:256,0 +DA:260,1 +DA:261,6 +DA:263,2 +DA:264,2 +DA:266,2 +DA:267,1 +DA:269,1 +DA:270,1 +DA:273,1 +DA:274,2 +DA:276,1 +DA:277,1 +DA:280,1 +DA:281,1 +DA:282,2 +DA:283,1 +DA:284,1 +DA:287,0 +DA:289,0 +DA:292,0 +DA:295,0 +DA:301,0 +DA:303,0 +DA:317,4 +DA:319,2 +DA:320,2 +DA:322,2 +DA:323,2 +DA:335,0 +DA:337,0 +DA:338,0 +DA:339,0 +DA:341,0 +DA:344,2 +DA:346,4 +LF:93 +LH:61 +end_of_record diff --git a/lib/src/hook_widget.dart b/lib/src/hook_widget.dart index cb194db1..061a91d6 100644 --- a/lib/src/hook_widget.dart +++ b/lib/src/hook_widget.dart @@ -130,8 +130,6 @@ abstract class Hook { } /// The logic and internal state for a [HookWidget] -/// -/// A [HookState] abstract class HookState> { /// Equivalent of [State.context] for [HookState] @protected @@ -187,7 +185,7 @@ class HookElement extends StatefulElement implements HookContext { @override void performRebuild() { _currentHook = _hooks?.iterator; - // first iterator always has null for unknown reasons + // first iterator always has null _currentHook?.moveNext(); assert(() { _debugHooksIndex = 0; diff --git a/test/hook_builder_test.dart b/test/hook_builder_test.dart index 414866d2..70d30e20 100644 --- a/test/hook_builder_test.dart +++ b/test/hook_builder_test.dart @@ -24,4 +24,12 @@ void main() { await tester.pumpWidget(createBuilder()); verify(fn.call(any)).called(1); }); + + test('builder required', () { + expect( + // ignore: missing_required_param, prefer_const_constructors + () => HookBuilder(), + throwsAssertionError, + ); + }); } diff --git a/test/hook_widget_test.dart b/test/hook_widget_test.dart index 701d126d..6c843b6a 100644 --- a/test/hook_widget_test.dart +++ b/test/hook_widget_test.dart @@ -1,25 +1,82 @@ // ignore_for_file: invalid_use_of_protected_member +import 'package:flutter/widgets.dart'; +import 'package:flutter_hooks/hook.dart'; + +import 'mock.dart'; + void main() { - // testWidgets('do not throw', (tester) { - // tester.pumpWidget(HookBuilder( - // builder: (context) { - // return Container(); - // }, - // )); - // }); - // testWidgets('simple hook', (tester) async { - // final state = _MockHookState(); - // when(state.build(any)).thenReturn(42); - - // int value; - // await tester.pumpWidget(HookBuilder( - // builder: (context) { - // value = context.use(_MockHook(state)); - // return Container(); - // }, - // )); - - // expect(value, equals(42)); - // }); + final build = Func1(); + final dispose = Func0(); + final initHook = Func0(); + final didUpdateHook = Func1(); + + final createHook = (HookContext context) => HookTest( + build: build.call, + dispose: dispose.call, + didUpdateHook: didUpdateHook.call, + initHook: initHook.call); + + tearDown(() { + clearInteractions(build); + clearInteractions(dispose); + clearInteractions(initHook); + clearInteractions(didUpdateHook); + }); + + group('life-cycles', () { + testWidgets('classic', (tester) async { + final builder = Func1(); + int result; + HookTest previousHook; + + when(build.call(any)).thenReturn(42); + when(builder.call(any)).thenAnswer((invocation) { + HookContext context = invocation.positionalArguments[0]; + previousHook = createHook(context); + result = context.use(previousHook); + return Container(); + }); + + await tester.pumpWidget(HookBuilder( + builder: builder.call, + )); + + expect(result, 42); + verifyInOrder([ + initHook.call() as dynamic, + build.call(any), + ]); + verifyZeroInteractions(didUpdateHook); + verifyZeroInteractions(dispose); + + when(build.call(any)).thenReturn(24); + await tester.pumpWidget(HookBuilder( + builder: builder.call, + )); + + expect(result, 24); + verifyInOrder([ + // TODO: previousHook instead of any + didUpdateHook.call(any) as dynamic, + build.call(any), + ]); + verifyNever(initHook.call()); + verifyZeroInteractions(dispose); + + await tester.pump(); + + verifyNever(initHook.call()); + verifyNever(didUpdateHook.call(any)); + verifyNever(build.call(any)); + verifyZeroInteractions(dispose); + + await tester.pumpWidget(const SizedBox()); + + verifyNever(initHook.call()); + verifyNever(didUpdateHook.call(any)); + verifyNever(build.call(any)); + verify(dispose.call()); + }); + }); } diff --git a/test/mock.dart b/test/mock.dart index 740ea573..17656267 100644 --- a/test/mock.dart +++ b/test/mock.dart @@ -2,6 +2,7 @@ import 'package:flutter_hooks/hook.dart'; import 'package:mockito/mockito.dart'; export 'package:flutter_test/flutter_test.dart' hide Func0, Func1; +export 'package:mockito/mockito.dart'; class MockHook extends Hook { final MockHookState state; @@ -25,3 +26,54 @@ abstract class _Func1 { } class Func1 extends Mock implements _Func1 {} + +class HookTest extends Hook { + final R Function(HookContext context) build; + final void Function() dispose; + final void Function() initHook; + final void Function(HookTest previousHook) didUpdateHook; + + HookTest({ + this.build, + this.dispose, + this.initHook, + this.didUpdateHook, + }) : super(); + + @override + HookStateTest createState() => HookStateTest(); +} + +class HookStateTest extends HookState> { + @override + void initHook() { + super.initHook(); + if (hook.initHook != null) { + hook.initHook(); + } + } + + @override + void dispose() { + super.dispose(); + if (hook.dispose != null) { + hook.dispose(); + } + } + + @override + void didUpdateHook(HookTest oldHook) { + super.didUpdateHook(oldHook); + if (hook.dispose != null) { + hook.didUpdateHook(oldHook); + } + } + + @override + R build(HookContext context) { + if (hook.build != null) { + return hook.build(context); + } + return null; + } +} From 21e1a070c5f817ab1717dee2bd0fa99edee8d8f9 Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Mon, 17 Dec 2018 10:53:16 +0100 Subject: [PATCH 004/384] remove coverage --- coverage/lcov.info | 148 --------------------------------------------- 1 file changed, 148 deletions(-) delete mode 100644 coverage/lcov.info diff --git a/coverage/lcov.info b/coverage/lcov.info deleted file mode 100644 index 42891949..00000000 --- a/coverage/lcov.info +++ /dev/null @@ -1,148 +0,0 @@ -SF:lib/src/hook_impl.dart -DA:7,0 -DA:8,0 -DA:9,0 -DA:11,0 -DA:12,0 -DA:18,0 -DA:20,0 -DA:21,0 -DA:24,0 -DA:26,0 -DA:27,0 -DA:28,0 -DA:29,0 -DA:30,0 -DA:34,0 -DA:35,0 -DA:36,0 -DA:43,0 -DA:45,0 -DA:53,0 -DA:54,0 -DA:56,0 -DA:57,0 -DA:64,0 -DA:66,0 -DA:67,0 -DA:68,0 -DA:72,0 -DA:74,0 -DA:81,0 -DA:83,0 -DA:84,0 -DA:90,0 -DA:92,0 -DA:93,0 -DA:96,0 -DA:98,0 -DA:99,0 -DA:102,0 -DA:104,0 -DA:107,0 -DA:108,0 -DA:123,2 -DA:126,1 -DA:127,2 -DA:129,2 -DA:130,2 -LF:47 -LH:5 -end_of_record -SF:lib/src/hook_widget.dart -DA:115,1 -DA:135,0 -DA:136,0 -DA:140,2 -DA:144,1 -DA:145,0 -DA:148,1 -DA:149,0 -DA:154,0 -DA:155,0 -DA:158,1 -DA:159,0 -DA:162,0 -DA:163,0 -DA:165,0 -DA:180,4 -DA:182,2 -DA:183,2 -DA:185,2 -DA:187,5 -DA:189,3 -DA:190,2 -DA:191,2 -DA:192,2 -DA:193,2 -DA:194,2 -DA:196,2 -DA:197,2 -DA:198,2 -DA:199,2 -DA:200,2 -DA:201,2 -DA:203,2 -DA:206,2 -DA:208,2 -DA:209,2 -DA:210,2 -DA:212,1 -DA:214,0 -DA:218,0 -DA:225,1 -DA:227,2 -DA:234,1 -DA:235,2 -DA:236,1 -DA:237,2 -DA:238,2 -DA:241,1 -DA:242,1 -DA:245,0 -DA:247,0 -DA:248,0 -DA:249,0 -DA:252,0 -DA:253,0 -DA:254,0 -DA:255,0 -DA:256,0 -DA:260,1 -DA:261,6 -DA:263,2 -DA:264,2 -DA:266,2 -DA:267,1 -DA:269,1 -DA:270,1 -DA:273,1 -DA:274,2 -DA:276,1 -DA:277,1 -DA:280,1 -DA:281,1 -DA:282,2 -DA:283,1 -DA:284,1 -DA:287,0 -DA:289,0 -DA:292,0 -DA:295,0 -DA:301,0 -DA:303,0 -DA:317,4 -DA:319,2 -DA:320,2 -DA:322,2 -DA:323,2 -DA:335,0 -DA:337,0 -DA:338,0 -DA:339,0 -DA:341,0 -DA:344,2 -DA:346,4 -LF:93 -LH:61 -end_of_record From 90882b32f6dfd0ce5329e341c77ff32c27853d55 Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Mon, 17 Dec 2018 10:53:42 +0100 Subject: [PATCH 005/384] update ignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index e9e8b166..0e13fdfe 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,4 @@ build/ android/ ios/ +/coverage From 0872b17d27ac03ae3002254836c818ab1f139a4a Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Mon, 17 Dec 2018 22:59:16 +0100 Subject: [PATCH 006/384] test --- lib/src/hook_widget.dart | 7 +- test/hook_widget_test.dart | 189 +++++++++++++++++++++++++------------ test/mock.dart | 19 ++++ 3 files changed, 155 insertions(+), 60 deletions(-) diff --git a/lib/src/hook_widget.dart b/lib/src/hook_widget.dart index 061a91d6..b6da0997 100644 --- a/lib/src/hook_widget.dart +++ b/lib/src/hook_widget.dart @@ -156,7 +156,7 @@ abstract class HookState> { /// Equivalent of [State.didUpdateWidget] for [HookState] @protected - void didUpdateHook(covariant Hook oldHook) {} + void didUpdateHook(covariant Hook oldHook) {} /// Equivalent of [State.setState] for [HookState] @protected @@ -224,6 +224,7 @@ class HookElement extends StatefulElement implements HookContext { @override R use(Hook hook) { + // TODO: test assert(_debugIsBuilding == true, ''' Hooks should only be called within the build method of a widget. Calling them outside of build method leads to an unstable state and is therefore prohibited @@ -232,12 +233,14 @@ class HookElement extends StatefulElement implements HookContext { HookState> hookState; // first build if (_currentHook == null) { + // TODO: test assert(_didReassemble || _isFirstBuild); hookState = _createHookState(hook); _hooks ??= []; _hooks.add(hookState); } else { // recreate states on hot-reload of the order changed + // TODO: test assert(() { if (!_didReassemble) { return true; @@ -264,7 +267,7 @@ class HookElement extends StatefulElement implements HookContext { _currentHook.moveNext(); if (hookState._hook != hook) { - final Hook previousHook = hookState._hook; + final previousHook = hookState._hook; hookState .._hook = hook ..didUpdateHook(previousHook); diff --git a/test/hook_widget_test.dart b/test/hook_widget_test.dart index 6c843b6a..0b538a65 100644 --- a/test/hook_widget_test.dart +++ b/test/hook_widget_test.dart @@ -11,72 +11,145 @@ void main() { final initHook = Func0(); final didUpdateHook = Func1(); - final createHook = (HookContext context) => HookTest( - build: build.call, - dispose: dispose.call, - didUpdateHook: didUpdateHook.call, - initHook: initHook.call); + final createHook = ( + HookContext context, { + void mockDispose(), + }) => + HookTest( + build: build.call, + dispose: mockDispose ?? dispose.call, + didUpdateHook: didUpdateHook.call, + initHook: initHook.call); tearDown(() { clearInteractions(build); clearInteractions(dispose); clearInteractions(initHook); clearInteractions(didUpdateHook); + + reset(build); + reset(dispose); + reset(initHook); + reset(didUpdateHook); + }); + + testWidgets('life-cycles in order', (tester) async { + final builder = Func1(); + int result; + HookTest previousHook; + + when(build.call(any)).thenReturn(42); + when(builder.call(any)).thenAnswer((invocation) { + HookContext context = invocation.positionalArguments[0]; + previousHook = createHook(context); + result = context.use(previousHook); + return Container(); + }); + + await tester.pumpWidget(HookBuilder( + builder: builder.call, + )); + + expect(result, 42); + verifyInOrder([ + initHook.call(), + build.call(any), + ]); + verifyZeroInteractions(didUpdateHook); + verifyZeroInteractions(dispose); + + when(build.call(any)).thenReturn(24); + await tester.pumpWidget(HookBuilder( + builder: builder.call, + )); + + expect(result, 24); + verifyInOrder([ + // ignore: todo + // TODO: previousHook instead of any + didUpdateHook.call(any), + build.call(any), + ]); + verifyNever(initHook.call()); + verifyZeroInteractions(dispose); + + await tester.pump(); + + verifyNever(initHook.call()); + verifyNever(didUpdateHook.call(any)); + verifyNever(build.call(any)); + verifyZeroInteractions(dispose); + + await tester.pumpWidget(const SizedBox()); + + verifyNever(initHook.call()); + verifyNever(didUpdateHook.call(any)); + verifyNever(build.call(any)); + verify(dispose.call()); }); - group('life-cycles', () { - testWidgets('classic', (tester) async { - final builder = Func1(); - int result; - HookTest previousHook; - - when(build.call(any)).thenReturn(42); - when(builder.call(any)).thenAnswer((invocation) { - HookContext context = invocation.positionalArguments[0]; - previousHook = createHook(context); - result = context.use(previousHook); - return Container(); - }); - - await tester.pumpWidget(HookBuilder( - builder: builder.call, - )); - - expect(result, 42); - verifyInOrder([ - initHook.call() as dynamic, - build.call(any), - ]); - verifyZeroInteractions(didUpdateHook); - verifyZeroInteractions(dispose); - - when(build.call(any)).thenReturn(24); - await tester.pumpWidget(HookBuilder( - builder: builder.call, - )); - - expect(result, 24); - verifyInOrder([ - // TODO: previousHook instead of any - didUpdateHook.call(any) as dynamic, - build.call(any), - ]); - verifyNever(initHook.call()); - verifyZeroInteractions(dispose); - - await tester.pump(); - - verifyNever(initHook.call()); - verifyNever(didUpdateHook.call(any)); - verifyNever(build.call(any)); - verifyZeroInteractions(dispose); - - await tester.pumpWidget(const SizedBox()); - - verifyNever(initHook.call()); - verifyNever(didUpdateHook.call(any)); - verifyNever(build.call(any)); - verify(dispose.call()); + testWidgets('dispose all called even on failed', (tester) async { + final builder = Func1(); + final dispose2 = Func0(); + final onError = Func1(); + + when(build.call(any)).thenReturn(42); + when(builder.call(any)).thenAnswer((invocation) { + HookContext context = invocation.positionalArguments[0]; + context + ..use(createHook(context)) + ..use(createHook(context, mockDispose: dispose2)); + return Container(); }); + when(dispose.call()).thenThrow(42); + + await tester.pumpWidget(HookBuilder(builder: builder.call)); + + FlutterError.onError = onError.call; + await tester.pumpWidget(const SizedBox()); + FlutterError.onError = FlutterError.dumpErrorToConsole; + + verifyInOrder([ + dispose.call(), + onError.call(argThat(isInstanceOf())), + dispose2.call(), + ]); + }); + + testWidgets('hot-reload triggers a build', (tester) async { + final builder = Func1(); + int result; + HookTest previousHook; + + when(build.call(any)).thenReturn(42); + when(builder.call(any)).thenAnswer((invocation) { + HookContext context = invocation.positionalArguments[0]; + previousHook = createHook(context); + result = context.use(previousHook); + return Container(); + }); + + await tester.pumpWidget(HookBuilder(builder: builder.call)); + + expect(result, 42); + verifyInOrder([ + initHook.call(), + build.call(any), + ]); + verifyZeroInteractions(didUpdateHook); + verifyZeroInteractions(dispose); + + when(build.call(any)).thenReturn(24); + + hotReload(tester); + await tester.pump(); + + expect(result, 24); + verifyInOrder([ + didUpdateHook.call(any), + build.call(any), + ]); + verifyNever(initHook.call()); + verifyNever(dispose.call()); }); } diff --git a/test/mock.dart b/test/mock.dart index 17656267..4aa1a64d 100644 --- a/test/mock.dart +++ b/test/mock.dart @@ -1,4 +1,6 @@ +import 'package:flutter/widgets.dart'; import 'package:flutter_hooks/hook.dart'; +import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/mockito.dart'; export 'package:flutter_test/flutter_test.dart' hide Func0, Func1; @@ -77,3 +79,20 @@ class HookStateTest extends HookState> { return null; } } + +Element _rootOf(Element element) { + Element root; + element.visitAncestorElements((e) { + if (e != null) { + root = e; + } + return e != null; + }); + return root; +} + +void hotReload(WidgetTester tester) { + final root = _rootOf(tester.allElements.first); + + TestWidgetsFlutterBinding.ensureInitialized().buildOwner..reassemble(root); +} From 5706d4881e9a1e8c50faf5479a2a0fc6341fc940 Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Tue, 18 Dec 2018 00:07:36 +0100 Subject: [PATCH 007/384] tests --- .travis.yml | 2 +- lib/src/hook_widget.dart | 8 ++- test/hook_widget_test.dart | 131 ++++++++++++++++++++++++++++++++----- 3 files changed, 120 insertions(+), 21 deletions(-) diff --git a/.travis.yml b/.travis.yml index 29933add..1dc7a077 100644 --- a/.travis.yml +++ b/.travis.yml @@ -15,7 +15,7 @@ script: - flutter packages get - flutter format --set-exit-if-changed lib example - flutter analyze --no-current-package lib example - - flutter test --no-pub + - flutter test --no-pub --coverage # export coverage - bash <(curl -s https://codecov.io/bash) cache: diff --git a/lib/src/hook_widget.dart b/lib/src/hook_widget.dart index b6da0997..d9695c34 100644 --- a/lib/src/hook_widget.dart +++ b/lib/src/hook_widget.dart @@ -195,6 +195,12 @@ class HookElement extends StatefulElement implements HookContext { return true; }()); super.performRebuild(); + assert(_debugHooksIndex == (_hooks?.length ?? 0), ''' +Build for $widget finished with less hooks used than a previous build. + +This may happen if the call to `use` is made under some condition. +Remove that condition to fix this error. +'''); assert(() { _isFirstBuild = false; _didReassemble = false; @@ -224,7 +230,6 @@ class HookElement extends StatefulElement implements HookContext { @override R use(Hook hook) { - // TODO: test assert(_debugIsBuilding == true, ''' Hooks should only be called within the build method of a widget. Calling them outside of build method leads to an unstable state and is therefore prohibited @@ -233,7 +238,6 @@ class HookElement extends StatefulElement implements HookContext { HookState> hookState; // first build if (_currentHook == null) { - // TODO: test assert(_didReassemble || _isFirstBuild); hookState = _createHookState(hook); _hooks ??= []; diff --git a/test/hook_widget_test.dart b/test/hook_widget_test.dart index 0b538a65..879d11c3 100644 --- a/test/hook_widget_test.dart +++ b/test/hook_widget_test.dart @@ -10,9 +10,10 @@ void main() { final dispose = Func0(); final initHook = Func0(); final didUpdateHook = Func1(); + final onError = Func1(); + final builder = Func1(); - final createHook = ( - HookContext context, { + final createHook = ({ void mockDispose(), }) => HookTest( @@ -22,11 +23,8 @@ void main() { initHook: initHook.call); tearDown(() { - clearInteractions(build); - clearInteractions(dispose); - clearInteractions(initHook); - clearInteractions(didUpdateHook); - + reset(builder); + reset(onError); reset(build); reset(dispose); reset(initHook); @@ -34,14 +32,13 @@ void main() { }); testWidgets('life-cycles in order', (tester) async { - final builder = Func1(); int result; HookTest previousHook; when(build.call(any)).thenReturn(42); when(builder.call(any)).thenAnswer((invocation) { HookContext context = invocation.positionalArguments[0]; - previousHook = createHook(context); + previousHook = createHook(); result = context.use(previousHook); return Container(); }); @@ -89,25 +86,23 @@ void main() { }); testWidgets('dispose all called even on failed', (tester) async { - final builder = Func1(); final dispose2 = Func0(); - final onError = Func1(); when(build.call(any)).thenReturn(42); when(builder.call(any)).thenAnswer((invocation) { - HookContext context = invocation.positionalArguments[0]; - context - ..use(createHook(context)) - ..use(createHook(context, mockDispose: dispose2)); + invocation.positionalArguments[0] + ..use(createHook()) + ..use(createHook(mockDispose: dispose2)); return Container(); }); when(dispose.call()).thenThrow(42); await tester.pumpWidget(HookBuilder(builder: builder.call)); + final previousErrorHandler = FlutterError.onError; FlutterError.onError = onError.call; await tester.pumpWidget(const SizedBox()); - FlutterError.onError = FlutterError.dumpErrorToConsole; + FlutterError.onError = previousErrorHandler; verifyInOrder([ dispose.call(), @@ -116,15 +111,115 @@ void main() { ]); }); + testWidgets('hook update with same instance do not call didUpdateHook', + (tester) async { + final hook = createHook(); + + when(builder.call(any)).thenAnswer((invocation) { + invocation.positionalArguments[0].use(hook); + return Container(); + }); + + await tester.pumpWidget(HookBuilder(builder: builder.call)); + + verifyInOrder([ + initHook.call(), + build.call(any), + ]); + verifyZeroInteractions(didUpdateHook); + verifyZeroInteractions(dispose); + + await tester.pumpWidget(HookBuilder(builder: builder.call)); + + verifyInOrder([ + build.call(any), + ]); + verifyNever(didUpdateHook.call(any)); + verifyNever(initHook.call()); + verifyNever(dispose.call()); + }); + + testWidgets('rebuild with different hooks crash', (tester) async { + when(builder.call(any)).thenAnswer((invocation) { + invocation.positionalArguments[0].use(HookTest()); + return Container(); + }); + + await tester.pumpWidget(HookBuilder(builder: builder.call)); + + when(builder.call(any)).thenAnswer((invocation) { + invocation.positionalArguments[0].use(HookTest()); + return Container(); + }); + + final previousErrorHandler = FlutterError.onError; + FlutterError.onError = onError.call; + await tester.pumpWidget(HookBuilder(builder: builder.call)); + FlutterError.onError = previousErrorHandler; + + verify(onError.call(any)); + }); + testWidgets('rebuild added hooks crash', (tester) async { + when(builder.call(any)).thenAnswer((invocation) { + invocation.positionalArguments[0].use(HookTest()); + return Container(); + }); + + await tester.pumpWidget(HookBuilder(builder: builder.call)); + + when(builder.call(any)).thenAnswer((invocation) { + invocation.positionalArguments[0].use(HookTest()); + invocation.positionalArguments[0].use(HookTest()); + return Container(); + }); + + final previousErrorHandler = FlutterError.onError; + FlutterError.onError = onError.call; + await tester.pumpWidget(HookBuilder(builder: builder.call)); + FlutterError.onError = previousErrorHandler; + + verify(onError.call(any)); + }); + + testWidgets('rebuild removed hooks crash', (tester) async { + when(builder.call(any)).thenAnswer((invocation) { + invocation.positionalArguments[0].use(HookTest()); + return Container(); + }); + + await tester.pumpWidget(HookBuilder(builder: builder.call)); + + when(builder.call(any)).thenAnswer((invocation) { + return Container(); + }); + + final previousErrorHandler = FlutterError.onError; + FlutterError.onError = onError.call; + await tester.pumpWidget(HookBuilder(builder: builder.call)); + FlutterError.onError = previousErrorHandler; + + verify(onError.call(any)); + }); + + testWidgets('use call outside build crash', (tester) async { + when(builder.call(any)).thenAnswer((invocation) => Container()); + + await tester.pumpWidget(HookBuilder(builder: builder.call)); + + final context = + tester.firstElement(find.byType(HookBuilder)) as HookElement; + + expect(() => context.use(HookTest()), throwsAssertionError); + }); + testWidgets('hot-reload triggers a build', (tester) async { - final builder = Func1(); int result; HookTest previousHook; when(build.call(any)).thenReturn(42); when(builder.call(any)).thenAnswer((invocation) { HookContext context = invocation.positionalArguments[0]; - previousHook = createHook(context); + previousHook = createHook(); result = context.use(previousHook); return Container(); }); From a758772a4ea3c36d4db2f6c29c812e631c558ca5 Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Tue, 18 Dec 2018 00:42:48 +0100 Subject: [PATCH 008/384] partial readme --- CHANGELOG.md | 3 +++ LICENSE | 21 ++++++++++++++++ README.md | 71 ++++++++++++++++++++++++++++++++++++++++++++++++++++ pubspec.yaml | 16 ++++++------ 4 files changed, 103 insertions(+), 8 deletions(-) create mode 100644 CHANGELOG.md create mode 100644 LICENSE diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..0df12e43 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,3 @@ +## 0.0.0: + +- initial release \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..cdeef1d5 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2018 Remi Rousselet + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md index e69de29b..05ca7faa 100644 --- a/README.md +++ b/README.md @@ -0,0 +1,71 @@ +[![Build Status](https://travis-ci.org/rrousselGit/flutter_hooks.svg?branch=master)](https://travis-ci.org/rrousselGit/flutter_hooks) [![codecov](https://codecov.io/gh/rrousselGit/flutter_hooks/branch/master/graph/badge.svg)](https://codecov.io/gh/rrousselGit/flutter_hooks) +[![pub package](https://img.shields.io/pub/v/flutter_hooks.svg)](https://pub.dartlang.org/packages/flutter_hooks) + + +# Flutter Hooks + +A flutter implementation of React hooks: https://medium.com/@dan_abramov/making-sense-of-react-hooks-fdbde8803889 + + +## What are hooks? + +Hooks are a new kind of object that manages a `Widget` life-cycles. They exists for one reason: increase the code sharing _between_ widgets and as a complete replacement for `StatefulWidget`. + +### The StatefulWidget issue + +`StatefulWidget` suffer from a big problem: it is very difficult reuse the logic of say `initState` or `dispose`. An obvious example is `AnimationController`: + +```dart +class Example extends StatefulWidget { + @override + _ExampleState createState() => _ExampleState(); +} + +class _ExampleState extends State + with SingleTickerProviderStateMixin { + AnimationController _controller; + + @override + void initState() { + super.initState(); + _controller = AnimationController(vsync: this); + } + + @override + void dispose() { + super.dispose(); + _controller.dispose(); + } + + @override + Widget build(BuildContext context) { + return Container(); + } +} +``` + +All widgets that desired to use an `AnimationController` will have to copy-paste the `initState`/`dispose`, which is of course undesired. + + +Dart mixins can partially solve this issue, but they are the source of another problem: type conflicts. If two mixins defines the same variable, the behavior may vary from a compilation fail to a totally unexpected behavior. + + +### The Hook solution + +Hooks are designed so that we can reuse the `initState`/`dispose` logic shown before between widgets. But without the potential issues of a mixin. + +*Hooks are independents and can be reused as many times as desired.* + +This means that with hooks, the equivalent of the previous code is: + +```dart +class Example extends HookWidget { + @override + Widget build(HookContext context) { + final controller = context.useAnimationController( + duration: const Duration(seconds: 1), + ); + return Container(); + } +} +``` \ No newline at end of file diff --git a/pubspec.yaml b/pubspec.yaml index 162ba72d..72685141 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,17 +1,17 @@ name: flutter_hooks -description: A new Flutter project. +description: A flutter implementation of React hooks. It adds a new kind of widget with enhanced code reuse. version: 0.0.0+1 environment: - sdk: ">=2.0.0-dev.68.0 <3.0.0" + sdk: '>=2.0.0-dev.68.0 <3.0.0' dependencies: - flutter: - sdk: flutter + flutter: + sdk: flutter dev_dependencies: - flutter_test: - sdk: flutter - pedantic: 1.4.0 - mockito: ">=4.0.0 <5.0.0" + flutter_test: + sdk: flutter + pedantic: 1.4.0 + mockito: '>=4.0.0 <5.0.0' From 1c032f863476e0379c17968c4cf1d7bf64116b80 Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Tue, 18 Dec 2018 00:46:44 +0100 Subject: [PATCH 009/384] pub initial publish --- example/lib/main.dart | 2 +- lib/{hook.dart => flutter_hooks.dart} | 0 pubspec.yaml | 2 ++ test/hook_builder_test.dart | 2 +- test/hook_widget_test.dart | 2 +- test/mock.dart | 2 +- 6 files changed, 6 insertions(+), 4 deletions(-) rename lib/{hook.dart => flutter_hooks.dart} (100%) diff --git a/example/lib/main.dart b/example/lib/main.dart index 03aaa6b9..91c5736f 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:flutter_hooks/hook.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; void main() => runApp(_MyApp()); diff --git a/lib/hook.dart b/lib/flutter_hooks.dart similarity index 100% rename from lib/hook.dart rename to lib/flutter_hooks.dart diff --git a/pubspec.yaml b/pubspec.yaml index 72685141..3cd22be0 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,5 +1,7 @@ name: flutter_hooks description: A flutter implementation of React hooks. It adds a new kind of widget with enhanced code reuse. +homepage: https://github.com/rrousselGit/flutter_hooks +author: Remi Rousselet version: 0.0.0+1 diff --git a/test/hook_builder_test.dart b/test/hook_builder_test.dart index 70d30e20..16608c56 100644 --- a/test/hook_builder_test.dart +++ b/test/hook_builder_test.dart @@ -1,5 +1,5 @@ import 'package:flutter/widgets.dart'; -import 'package:flutter_hooks/hook.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:mockito/mockito.dart'; import 'mock.dart'; diff --git a/test/hook_widget_test.dart b/test/hook_widget_test.dart index 879d11c3..65e3a7b3 100644 --- a/test/hook_widget_test.dart +++ b/test/hook_widget_test.dart @@ -1,7 +1,7 @@ // ignore_for_file: invalid_use_of_protected_member import 'package:flutter/widgets.dart'; -import 'package:flutter_hooks/hook.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; import 'mock.dart'; diff --git a/test/mock.dart b/test/mock.dart index 4aa1a64d..b2b35da8 100644 --- a/test/mock.dart +++ b/test/mock.dart @@ -1,5 +1,5 @@ import 'package:flutter/widgets.dart'; -import 'package:flutter_hooks/hook.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/mockito.dart'; From c489deae416f34e4a635c52fbe1f08b3e41d698e Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Tue, 18 Dec 2018 12:23:46 +0100 Subject: [PATCH 010/384] test useMemoized --- lib/src/hook_impl.dart | 6 +- lib/src/hook_widget.dart | 5 +- test/memoized_test.dart | 249 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 254 insertions(+), 6 deletions(-) create mode 100644 test/memoized_test.dart diff --git a/lib/src/hook_impl.dart b/lib/src/hook_impl.dart index fd76c580..23ff13a3 100644 --- a/lib/src/hook_impl.dart +++ b/lib/src/hook_impl.dart @@ -1,7 +1,7 @@ part of 'hook.dart'; class _MemoizedHook extends Hook { - final T Function(T oldValue) valueBuilder; + final T Function() valueBuilder; final List parameters; const _MemoizedHook(this.valueBuilder, {this.parameters = const []}) @@ -18,7 +18,7 @@ class _MemoizedHookState extends HookState> { @override void initHook() { super.initHook(); - value = hook.valueBuilder(null); + value = hook.valueBuilder(); } @override @@ -27,7 +27,7 @@ class _MemoizedHookState extends HookState> { if (hook.parameters != oldHook.parameters && (hook.parameters.length != oldHook.parameters.length || _hasDiffWith(oldHook.parameters))) { - value = hook.valueBuilder(value); + value = hook.valueBuilder(); } } diff --git a/lib/src/hook_widget.dart b/lib/src/hook_widget.dart index d9695c34..42ad17aa 100644 --- a/lib/src/hook_widget.dart +++ b/lib/src/hook_widget.dart @@ -297,8 +297,7 @@ Remove that condition to fix this error. } @override - T useMemoized(T Function(T oldValue) valueBuilder, - {List parameters = const []}) { + T useMemoized(T Function() valueBuilder, {List parameters = const []}) { return use(_MemoizedHook( valueBuilder, parameters: parameters, @@ -390,7 +389,7 @@ abstract class HookContext extends BuildContext { /// * [parameters] can be use to specify a list of objects for [useMemoized] to watch. /// So that whenever [operator==] fails on any parameter or if the length of [parameters] changes, /// [valueBuilder] is called again. - T useMemoized(T valueBuilder(T previousValue), {List parameters}); + T useMemoized(T valueBuilder(), {List parameters}); /// Watches a value. /// diff --git a/test/memoized_test.dart b/test/memoized_test.dart new file mode 100644 index 00000000..2194e8a3 --- /dev/null +++ b/test/memoized_test.dart @@ -0,0 +1,249 @@ +import 'package:flutter/widgets.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; + +import 'mock.dart'; + +void main() { + final builder = Func1(); + final parameterBuilder = Func0(); + final valueBuilder = Func0(); + + tearDown(() { + reset(builder); + reset(valueBuilder); + reset(parameterBuilder); + }); + + testWidgets('invalid parameters', (tester) async { + final onError = Func1(); + + var previousErrorHandler = FlutterError.onError; + FlutterError.onError = onError.call; + await tester.pumpWidget(HookBuilder(builder: (context) { + context.useMemoized(null); + return Container(); + })); + FlutterError.onError = previousErrorHandler; + + expect( + (verify(onError.call(captureAny)).captured.first as FlutterErrorDetails) + .exception, + isInstanceOf(), + ); + + previousErrorHandler = FlutterError.onError; + FlutterError.onError = onError.call; + await tester.pumpWidget(HookBuilder(builder: (context) { + context.useMemoized(() {}, parameters: null); + return Container(); + })); + FlutterError.onError = previousErrorHandler; + + expect( + (verify(onError.call(captureAny)).captured.first as FlutterErrorDetails) + .exception, + isInstanceOf(), + ); + }); + + testWidgets('memoized without parameter calls valueBuilder once', + (tester) async { + int result; + + when(valueBuilder.call()).thenReturn(42); + + when(builder.call(any)).thenAnswer((invocation) { + final HookContext context = invocation.positionalArguments.single; + result = context.useMemoized(valueBuilder.call); + return Container(); + }); + + await tester.pumpWidget(HookBuilder(builder: builder.call)); + + verify(valueBuilder.call()).called(1); + expect(result, 42); + + await tester.pumpWidget(HookBuilder(builder: builder.call)); + + verifyNever(valueBuilder.call()); + expect(result, 42); + + await tester.pumpWidget(const SizedBox()); + + verifyNever(valueBuilder.call()); + }); + + testWidgets( + 'memoized with parameter call valueBuilder again on parameter change', + (tester) async { + int result; + + when(valueBuilder.call()).thenReturn(0); + when(parameterBuilder.call()).thenReturn([]); + + when(builder.call(any)).thenAnswer((invocation) { + final HookContext context = invocation.positionalArguments.single; + result = context.useMemoized(valueBuilder.call, + parameters: parameterBuilder.call()); + return Container(); + }); + + await tester.pumpWidget(HookBuilder(builder: builder.call)); + + verify(valueBuilder.call()).called(1); + expect(result, 0); + + /* No change */ + + await tester.pumpWidget(HookBuilder(builder: builder.call)); + + verifyNever(valueBuilder.call()); + expect(result, 0); + + /* Add parameter */ + + when(parameterBuilder.call()).thenReturn(['foo']); + when(valueBuilder.call()).thenReturn(1); + + await tester.pumpWidget(HookBuilder(builder: builder.call)); + + expect(result, 1); + verify(valueBuilder.call()).called(1); + + /* No change */ + + await tester.pumpWidget(HookBuilder(builder: builder.call)); + + verifyNever(valueBuilder.call()); + expect(result, 1); + + /* Remove parameter */ + + when(parameterBuilder.call()).thenReturn([]); + when(valueBuilder.call()).thenReturn(2); + + await tester.pumpWidget(HookBuilder(builder: builder.call)); + + expect(result, 2); + verify(valueBuilder.call()).called(1); + + /* No change */ + + await tester.pumpWidget(HookBuilder(builder: builder.call)); + + verifyNever(valueBuilder.call()); + expect(result, 2); + + /* DISPOSE */ + + await tester.pumpWidget(const SizedBox()); + + verifyNever(valueBuilder.call()); + }); + + testWidgets('memoized parameters compared in order', (tester) async { + int result; + + when(builder.call(any)).thenAnswer((invocation) { + final HookContext context = invocation.positionalArguments.single; + result = context.useMemoized(valueBuilder.call, + parameters: parameterBuilder.call()); + return Container(); + }); + + when(valueBuilder.call()).thenReturn(0); + when(parameterBuilder.call()).thenReturn(['foo', 42, 24.0]); + + await tester.pumpWidget(HookBuilder(builder: builder.call)); + + verify(valueBuilder.call()).called(1); + expect(result, 0); + + /* Array reference changed but content didn't */ + + when(parameterBuilder.call()).thenReturn(['foo', 42, 24.0]); + await tester.pumpWidget(HookBuilder(builder: builder.call)); + + verifyNever(valueBuilder.call()); + expect(result, 0); + + /* reoder */ + + when(valueBuilder.call()).thenReturn(1); + when(parameterBuilder.call()).thenReturn([42, 'foo', 24.0]); + + await tester.pumpWidget(HookBuilder(builder: builder.call)); + + verify(valueBuilder.call()).called(1); + expect(result, 1); + + when(valueBuilder.call()).thenReturn(2); + when(parameterBuilder.call()).thenReturn([42, 24.0, 'foo']); + + await tester.pumpWidget(HookBuilder(builder: builder.call)); + + verify(valueBuilder.call()).called(1); + expect(result, 2); + + /* value change */ + + when(valueBuilder.call()).thenReturn(3); + when(parameterBuilder.call()).thenReturn([43, 24.0, 'foo']); + + await tester.pumpWidget(HookBuilder(builder: builder.call)); + + verify(valueBuilder.call()).called(1); + expect(result, 3); + + /* Comparison is done using operator== */ + + // type change + when(parameterBuilder.call()).thenReturn([43.0, 24.0, 'foo']); + + await tester.pumpWidget(HookBuilder(builder: builder.call)); + + verifyNever(valueBuilder.call()); + expect(result, 3); + + /* DISPOSE */ + + await tester.pumpWidget(const SizedBox()); + + verifyNever(valueBuilder.call()); + }); + + testWidgets( + 'memoized parameter reference do not change don\'t call valueBuilder', + (tester) async { + int result; + final parameters = []; + + when(builder.call(any)).thenAnswer((invocation) { + final HookContext context = invocation.positionalArguments.single; + result = context.useMemoized(valueBuilder.call, + parameters: parameterBuilder.call()); + return Container(); + }); + + when(valueBuilder.call()).thenReturn(0); + when(parameterBuilder.call()).thenReturn(parameters); + + await tester.pumpWidget(HookBuilder(builder: builder.call)); + + verify(valueBuilder.call()).called(1); + expect(result, 0); + + /* Array content but reference didn't */ + parameters.add(42); + + await tester.pumpWidget(HookBuilder(builder: builder.call)); + + verifyNever(valueBuilder.call()); + + /* DISPOSE */ + + await tester.pumpWidget(const SizedBox()); + + verifyNever(valueBuilder.call()); + }); +} From c61ad4ec542f6ac57db82cd615734351654884df Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Tue, 18 Dec 2018 12:51:56 +0100 Subject: [PATCH 011/384] factorized tests --- test/memoized_test.dart | 38 ++++++++++++-------------------------- test/mock.dart | 26 ++++++++++++++++++++++++++ 2 files changed, 38 insertions(+), 26 deletions(-) diff --git a/test/memoized_test.dart b/test/memoized_test.dart index 2194e8a3..9cedbdf0 100644 --- a/test/memoized_test.dart +++ b/test/memoized_test.dart @@ -15,34 +15,20 @@ void main() { }); testWidgets('invalid parameters', (tester) async { - final onError = Func1(); - - var previousErrorHandler = FlutterError.onError; - FlutterError.onError = onError.call; - await tester.pumpWidget(HookBuilder(builder: (context) { - context.useMemoized(null); - return Container(); - })); - FlutterError.onError = previousErrorHandler; - - expect( - (verify(onError.call(captureAny)).captured.first as FlutterErrorDetails) - .exception, - isInstanceOf(), + await expectPump( + () => tester.pumpWidget(HookBuilder(builder: (context) { + context.useMemoized(null); + return Container(); + })), + throwsAssertionError, ); - previousErrorHandler = FlutterError.onError; - FlutterError.onError = onError.call; - await tester.pumpWidget(HookBuilder(builder: (context) { - context.useMemoized(() {}, parameters: null); - return Container(); - })); - FlutterError.onError = previousErrorHandler; - - expect( - (verify(onError.call(captureAny)).captured.first as FlutterErrorDetails) - .exception, - isInstanceOf(), + await expectPump( + () => tester.pumpWidget(HookBuilder(builder: (context) { + context.useMemoized(() {}, parameters: null); + return Container(); + })), + throwsAssertionError, ); }); diff --git a/test/mock.dart b/test/mock.dart index b2b35da8..0f2638bb 100644 --- a/test/mock.dart +++ b/test/mock.dart @@ -96,3 +96,29 @@ void hotReload(WidgetTester tester) { TestWidgetsFlutterBinding.ensureInitialized().buildOwner..reassemble(root); } + +Future expectPump( + Future pump(), + dynamic matcher, { + String reason, + dynamic skip, +}) async { + FlutterErrorDetails details; + if (skip == null || skip != false) { + final previousErrorHandler = FlutterError.onError; + FlutterError.onError = (d) { + details = d; + }; + await pump(); + FlutterError.onError = previousErrorHandler; + } + + expect( + details != null + ? Future.error(details.exception) + : Future.value(), + matcher, + reason: reason, + skip: skip, + ); +} From 875567a64f005ec74b13d126ae7e41164308635b Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Tue, 18 Dec 2018 19:22:53 +0100 Subject: [PATCH 012/384] factorize tests --- lib/src/hook_widget.dart | 5 ++++- test/hook_widget_test.dart | 43 +++++++++++++++----------------------- test/mock.dart | 4 ++-- 3 files changed, 23 insertions(+), 29 deletions(-) diff --git a/lib/src/hook_widget.dart b/lib/src/hook_widget.dart index 42ad17aa..e2701f3a 100644 --- a/lib/src/hook_widget.dart +++ b/lib/src/hook_widget.dart @@ -142,10 +142,12 @@ abstract class HookState> { /// Equivalent of [State.initState] for [HookState] @protected + @mustCallSuper void initHook() {} /// Equivalent of [State.dispose] for [HookState] @protected + @mustCallSuper void dispose() {} /// Called everytimes the [HookState] is requested @@ -156,6 +158,7 @@ abstract class HookState> { /// Equivalent of [State.didUpdateWidget] for [HookState] @protected + @mustCallSuper void didUpdateHook(covariant Hook oldHook) {} /// Equivalent of [State.setState] for [HookState] @@ -265,7 +268,7 @@ Remove that condition to fix this error. return true; }()); - assert(_currentHook.current.hook.runtimeType == hook.runtimeType); + assert(_currentHook.current?.hook?.runtimeType == hook.runtimeType); hookState = _currentHook.current as HookState>; _currentHook.moveNext(); diff --git a/test/hook_widget_test.dart b/test/hook_widget_test.dart index 65e3a7b3..c0a765da 100644 --- a/test/hook_widget_test.dart +++ b/test/hook_widget_test.dart @@ -10,7 +10,6 @@ void main() { final dispose = Func0(); final initHook = Func0(); final didUpdateHook = Func1(); - final onError = Func1(); final builder = Func1(); final createHook = ({ @@ -24,7 +23,6 @@ void main() { tearDown(() { reset(builder); - reset(onError); reset(build); reset(dispose); reset(initHook); @@ -95,18 +93,17 @@ void main() { ..use(createHook(mockDispose: dispose2)); return Container(); }); - when(dispose.call()).thenThrow(42); await tester.pumpWidget(HookBuilder(builder: builder.call)); - final previousErrorHandler = FlutterError.onError; - FlutterError.onError = onError.call; - await tester.pumpWidget(const SizedBox()); - FlutterError.onError = previousErrorHandler; + when(dispose.call()).thenThrow(24); + await expectPump( + () => tester.pumpWidget(const SizedBox()), + throwsA(24), + ); verifyInOrder([ dispose.call(), - onError.call(argThat(isInstanceOf())), dispose2.call(), ]); }); @@ -152,12 +149,10 @@ void main() { return Container(); }); - final previousErrorHandler = FlutterError.onError; - FlutterError.onError = onError.call; - await tester.pumpWidget(HookBuilder(builder: builder.call)); - FlutterError.onError = previousErrorHandler; - - verify(onError.call(any)); + await expectPump( + () => tester.pumpWidget(HookBuilder(builder: builder.call)), + throwsAssertionError, + ); }); testWidgets('rebuild added hooks crash', (tester) async { when(builder.call(any)).thenAnswer((invocation) { @@ -173,12 +168,10 @@ void main() { return Container(); }); - final previousErrorHandler = FlutterError.onError; - FlutterError.onError = onError.call; - await tester.pumpWidget(HookBuilder(builder: builder.call)); - FlutterError.onError = previousErrorHandler; - - verify(onError.call(any)); + await expectPump( + () => tester.pumpWidget(HookBuilder(builder: builder.call)), + throwsAssertionError, + ); }); testWidgets('rebuild removed hooks crash', (tester) async { @@ -193,12 +186,10 @@ void main() { return Container(); }); - final previousErrorHandler = FlutterError.onError; - FlutterError.onError = onError.call; - await tester.pumpWidget(HookBuilder(builder: builder.call)); - FlutterError.onError = previousErrorHandler; - - verify(onError.call(any)); + await expectPump( + () => tester.pumpWidget(HookBuilder(builder: builder.call)), + throwsAssertionError, + ); }); testWidgets('use call outside build crash', (tester) async { diff --git a/test/mock.dart b/test/mock.dart index 0f2638bb..f560caa5 100644 --- a/test/mock.dart +++ b/test/mock.dart @@ -113,9 +113,9 @@ Future expectPump( FlutterError.onError = previousErrorHandler; } - expect( + await expectLater( details != null - ? Future.error(details.exception) + ? Future.error(details.exception, details.stack) : Future.value(), matcher, reason: reason, From de19b6e25b22da588b655beadc245d76aa1d99d6 Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Wed, 19 Dec 2018 13:54:25 +0100 Subject: [PATCH 013/384] improve and test reassemble --- lib/src/hook_widget.dart | 60 +++++++--- test/hook_widget_test.dart | 217 +++++++++++++++++++++++++++++++++++-- 2 files changed, 254 insertions(+), 23 deletions(-) diff --git a/lib/src/hook_widget.dart b/lib/src/hook_widget.dart index e2701f3a..d03f8e6e 100644 --- a/lib/src/hook_widget.dart +++ b/lib/src/hook_widget.dart @@ -178,6 +178,7 @@ class HookElement extends StatefulElement implements HookContext { bool _debugIsBuilding; bool _didReassemble; bool _isFirstBuild; + bool _debugShouldDispose; /// Creates an element that uses the given widget as its configuration. HookElement(HookWidget widget) : super(widget); @@ -191,6 +192,7 @@ class HookElement extends StatefulElement implements HookContext { // first iterator always has null _currentHook?.moveNext(); assert(() { + _debugShouldDispose = false; _debugHooksIndex = 0; _isFirstBuild ??= true; _didReassemble ??= false; @@ -198,11 +200,23 @@ class HookElement extends StatefulElement implements HookContext { return true; }()); super.performRebuild(); + + // dispose removed items + assert(() { + if (_didReassemble) { + while (_currentHook.current != null) { + _currentHook.current.dispose(); + _currentHook.moveNext(); + _debugHooksIndex++; + } + } + return true; + }()); assert(_debugHooksIndex == (_hooks?.length ?? 0), ''' Build for $widget finished with less hooks used than a previous build. - +Used $_debugHooksIndex hooks while a previous build had ${_hooks.length}. This may happen if the call to `use` is made under some condition. -Remove that condition to fix this error. + '''); assert(() { _isFirstBuild = false; @@ -247,25 +261,43 @@ Remove that condition to fix this error. _hooks.add(hookState); } else { // recreate states on hot-reload of the order changed - // TODO: test assert(() { if (!_didReassemble) { return true; } - if (_currentHook.current?.hook?.runtimeType == hook.runtimeType) { + if (!_debugShouldDispose && + _currentHook.current?.hook?.runtimeType == hook.runtimeType) { return true; - } else if (_currentHook.current != null) { - for (var i = _hooks.length - 1; i >= _debugHooksIndex; i--) { - _hooks.removeLast().dispose(); - } } - hookState = _createHookState(hook); - _hooks.add(hookState); - _currentHook = _hooks.iterator; - for (var i = 0; i < _hooks.length; i++) { - _currentHook.moveNext(); + _debugShouldDispose = true; + + // some previous hook has changed of type, so we dispose all the following states + // _currentHook.current can be null when reassemble is adding new hooks + if (_currentHook.current != null) { + _hooks.remove(_currentHook.current..dispose()); + // has to be done after the dispose call + hookState = _createHookState(hook); + // compensate for the `_debutHooksIndex++` at the end + _debugHooksIndex--; + _hooks.add(hookState); + + // we move the iterator back to where it was + _currentHook = _hooks.iterator; + for (var i = 0; + i + 2 < _hooks.length && _hooks[i + 2] != hookState; + i++) { + _currentHook.moveNext(); + } + } else { + hookState = _createHookState(hook); + _hooks.add(hookState); + + // we put the iterator on added item + _currentHook = _hooks.iterator; + while (_currentHook.current != hookState) { + _currentHook.moveNext(); + } } - return true; }()); assert(_currentHook.current?.hook?.runtimeType == hook.runtimeType); diff --git a/test/hook_widget_test.dart b/test/hook_widget_test.dart index c0a765da..456a37d1 100644 --- a/test/hook_widget_test.dart +++ b/test/hook_widget_test.dart @@ -12,14 +12,12 @@ void main() { final didUpdateHook = Func1(); final builder = Func1(); - final createHook = ({ - void mockDispose(), - }) => - HookTest( - build: build.call, - dispose: mockDispose ?? dispose.call, - didUpdateHook: didUpdateHook.call, - initHook: initHook.call); + final createHook = () => HookTest( + build: build.call, + dispose: dispose.call, + didUpdateHook: didUpdateHook.call, + initHook: initHook.call, + ); tearDown(() { reset(builder); @@ -90,7 +88,7 @@ void main() { when(builder.call(any)).thenAnswer((invocation) { invocation.positionalArguments[0] ..use(createHook()) - ..use(createHook(mockDispose: dispose2)); + ..use(HookTest(dispose: dispose2)); return Container(); }); @@ -238,4 +236,205 @@ void main() { verifyNever(initHook.call()); verifyNever(dispose.call()); }); + + testWidgets('hot-reload can add hooks', (tester) async { + HookTest hook1; + + final dispose2 = Func0(); + final initHook2 = Func0(); + final didUpdateHook2 = Func1(); + final build2 = Func1(); + + when(builder.call(any)).thenAnswer((invocation) { + (invocation.positionalArguments[0] as HookContext) + ..use(hook1 = createHook()); + return Container(); + }); + + await tester.pumpWidget(HookBuilder(builder: builder.call)); + + final HookElement context = find.byType(HookBuilder).evaluate().first; + + verifyInOrder([ + initHook.call(), + build.call(context), + ]); + verifyZeroInteractions(dispose); + verifyZeroInteractions(didUpdateHook); + + when(builder.call(any)).thenAnswer((invocation) { + (invocation.positionalArguments[0] as HookContext) + ..use(createHook()) + ..use(HookTest( + initHook: initHook2, + build: build2, + didUpdateHook: didUpdateHook2, + dispose: dispose2, + )); + return Container(); + }); + + hotReload(tester); + await tester.pump(); + + verifyInOrder([ + didUpdateHook.call(hook1), + build.call(context), + initHook2.call(), + build2.call(context), + ]); + verifyNoMoreInteractions(initHook); + verifyZeroInteractions(dispose); + verifyZeroInteractions(dispose2); + verifyZeroInteractions(didUpdateHook2); + }); + testWidgets('hot-reload can remove hooks', (tester) async { + final dispose2 = Func0(); + final initHook2 = Func0(); + final didUpdateHook2 = Func1(); + final build2 = Func1(); + + when(builder.call(any)).thenAnswer((invocation) { + (invocation.positionalArguments[0] as HookContext) + ..use(createHook()) + ..use(HookTest( + initHook: initHook2, + build: build2, + didUpdateHook: didUpdateHook2, + dispose: dispose2, + )); + return Container(); + }); + + await tester.pumpWidget(HookBuilder(builder: builder.call)); + final HookElement context = find.byType(HookBuilder).evaluate().first; + + verifyInOrder([ + initHook.call(), + build.call(context), + initHook2.call(), + build2.call(context), + ]); + + verifyZeroInteractions(dispose); + verifyZeroInteractions(didUpdateHook); + verifyZeroInteractions(dispose2); + verifyZeroInteractions(didUpdateHook2); + + when(builder.call(any)).thenAnswer((invocation) { + return Container(); + }); + + hotReload(tester); + await tester.pumpWidget(HookBuilder(builder: builder.call)); + + verifyInOrder([ + dispose.call(), + dispose2.call(), + ]); + + verifyNoMoreInteractions(initHook); + verifyNoMoreInteractions(initHook2); + verifyNoMoreInteractions(build2); + verifyNoMoreInteractions(build); + + verifyZeroInteractions(didUpdateHook); + verifyZeroInteractions(didUpdateHook2); + }); + testWidgets('hot-reload disposes hooks when type change', (tester) async { + HookTest hook1; + + final dispose2 = Func0(); + final initHook2 = Func0(); + final didUpdateHook2 = Func1(); + final build2 = Func1(); + + final dispose3 = Func0(); + final initHook3 = Func0(); + final didUpdateHook3 = Func1(); + final build3 = Func1(); + + final dispose4 = Func0(); + final initHook4 = Func0(); + final didUpdateHook4 = Func1(); + final build4 = Func1(); + + when(builder.call(any)).thenAnswer((invocation) { + (invocation.positionalArguments[0] as HookContext) + ..use(hook1 = createHook()) + ..use(HookTest(dispose: dispose2)) + ..use(HookTest(dispose: dispose3)) + ..use(HookTest(dispose: dispose4)); + return Container(); + }); + + await tester.pumpWidget(HookBuilder(builder: builder.call)); + + final HookElement context = find.byType(HookBuilder).evaluate().first; + + // We don't care about datas of the first render + clearInteractions(initHook); + clearInteractions(didUpdateHook); + clearInteractions(dispose); + clearInteractions(build); + + clearInteractions(initHook2); + clearInteractions(didUpdateHook2); + clearInteractions(dispose2); + clearInteractions(build2); + + clearInteractions(initHook3); + clearInteractions(didUpdateHook3); + clearInteractions(dispose3); + clearInteractions(build3); + + clearInteractions(initHook4); + clearInteractions(didUpdateHook4); + clearInteractions(dispose4); + clearInteractions(build4); + + when(builder.call(any)).thenAnswer((invocation) { + (invocation.positionalArguments[0] as HookContext) + ..use(createHook()) + // changed type from HookTest + ..use(HookTest( + initHook: initHook2, + build: build2, + didUpdateHook: didUpdateHook2, + )) + ..use(HookTest( + initHook: initHook3, + build: build3, + didUpdateHook: didUpdateHook3, + )) + ..use(HookTest( + initHook: initHook4, + build: build4, + didUpdateHook: didUpdateHook4, + )); + return Container(); + }); + + hotReload(tester); + await tester.pump(); + + verifyInOrder([ + didUpdateHook.call(hook1), + build.call(context), + dispose2.call(), + initHook2.call(), + build2.call(context), + dispose3.call(), + initHook3.call(), + build3.call(context), + dispose4.call(), + initHook4.call(), + build4.call(context), + ]); + verifyZeroInteractions(initHook); + verifyZeroInteractions(dispose); + verifyZeroInteractions(didUpdateHook2); + verifyZeroInteractions(didUpdateHook3); + verifyZeroInteractions(didUpdateHook4); + }); } From 3245208b4664f06ed5db8269ef46a1f6434820a5 Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Wed, 19 Dec 2018 14:05:47 +0100 Subject: [PATCH 014/384] bumb version --- example/pubspec.lock | 2 +- pubspec.yaml | 16 ++++++++-------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/example/pubspec.lock b/example/pubspec.lock index d6580f15..1858b454 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -40,7 +40,7 @@ packages: path: ".." relative: true source: path - version: "0.0.0+1" + version: "0.0.0+2" flutter_test: dependency: "direct dev" description: flutter diff --git a/pubspec.yaml b/pubspec.yaml index 3cd22be0..bf7ad2d8 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -3,17 +3,17 @@ description: A flutter implementation of React hooks. It adds a new kind of widg homepage: https://github.com/rrousselGit/flutter_hooks author: Remi Rousselet -version: 0.0.0+1 +version: 0.0.0+2 environment: - sdk: '>=2.0.0-dev.68.0 <3.0.0' + sdk: ">=2.0.0-dev.68.0 <3.0.0" dependencies: - flutter: - sdk: flutter + flutter: + sdk: flutter dev_dependencies: - flutter_test: - sdk: flutter - pedantic: 1.4.0 - mockito: '>=4.0.0 <5.0.0' + flutter_test: + sdk: flutter + pedantic: 1.4.0 + mockito: ">=4.0.0 <5.0.0" From 6406ab0cbcf797c1334de133691937dee6944296 Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Wed, 19 Dec 2018 16:45:35 +0100 Subject: [PATCH 015/384] fix coverage bug --- test/mock.dart | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/test/mock.dart b/test/mock.dart index f560caa5..712e00f5 100644 --- a/test/mock.dart +++ b/test/mock.dart @@ -6,17 +6,6 @@ import 'package:mockito/mockito.dart'; export 'package:flutter_test/flutter_test.dart' hide Func0, Func1; export 'package:mockito/mockito.dart'; -class MockHook extends Hook { - final MockHookState state; - - MockHook([MockHookState state]) : state = state ?? MockHookState(); - - @override - MockHookState createState() => state; -} - -class MockHookState extends Mock implements HookState> {} - abstract class _Func0 { R call(); } From 8034182b0b4443b7db6a4cbaf0122b8aeb121a13 Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Thu, 20 Dec 2018 13:45:18 +0100 Subject: [PATCH 016/384] image --- README.md | 9 ++++----- hooks.png | Bin 0 -> 13086 bytes 2 files changed, 4 insertions(+), 5 deletions(-) create mode 100644 hooks.png diff --git a/README.md b/README.md index 05ca7faa..8c063448 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,13 @@ [![Build Status](https://travis-ci.org/rrousselGit/flutter_hooks.svg?branch=master)](https://travis-ci.org/rrousselGit/flutter_hooks) [![codecov](https://codecov.io/gh/rrousselGit/flutter_hooks/branch/master/graph/badge.svg)](https://codecov.io/gh/rrousselGit/flutter_hooks) + [![pub package](https://img.shields.io/pub/v/flutter_hooks.svg)](https://pub.dartlang.org/packages/flutter_hooks) +![alt text](https://raw.githubusercontent.com/rrousselGit/flutter_hook/master/hooks.png) # Flutter Hooks A flutter implementation of React hooks: https://medium.com/@dan_abramov/making-sense-of-react-hooks-fdbde8803889 - ## What are hooks? Hooks are a new kind of object that manages a `Widget` life-cycles. They exists for one reason: increase the code sharing _between_ widgets and as a complete replacement for `StatefulWidget`. @@ -46,15 +47,13 @@ class _ExampleState extends State All widgets that desired to use an `AnimationController` will have to copy-paste the `initState`/`dispose`, which is of course undesired. - Dart mixins can partially solve this issue, but they are the source of another problem: type conflicts. If two mixins defines the same variable, the behavior may vary from a compilation fail to a totally unexpected behavior. - ### The Hook solution Hooks are designed so that we can reuse the `initState`/`dispose` logic shown before between widgets. But without the potential issues of a mixin. -*Hooks are independents and can be reused as many times as desired.* +_Hooks are independents and can be reused as many times as desired._ This means that with hooks, the equivalent of the previous code is: @@ -68,4 +67,4 @@ class Example extends HookWidget { return Container(); } } -``` \ No newline at end of file +``` diff --git a/hooks.png b/hooks.png new file mode 100644 index 0000000000000000000000000000000000000000..198120364c14e9b9c8edb5df32bbfa16e98ad9c7 GIT binary patch literal 13086 zcmb_jwL*|_S~~GbI)#~G(ITcVN+luAtB)@Daw6Bj8p$zn6D6b@1nEMhymI2qk;@l z%>>l};sMQ8T1^@WsUCv!WR8w_#&T2C_e4S>F#qpDE|V;^M?&IjRFad{_Axoh#_}hb zzlNF)7JD5%x3)a5I7|mspY$x>xEel|*2r5ct*0QdZHFjpx_$ayhZzRO3r+}=LXiQ` zIRyh`Xh%n}qv=npgBD28>3ko@Ua~ShBg8LHySk9rif$k8vnD%lS3PZ+@3S_5{Po_J|I!pNz36ex>CHq*^ z#Y;=R*LLqcXXnqMd^rpLvUdF%bHAgqrsEKoDyUsa#FU4Lai|=XnKKYQIl6B`;e70u zGwnpMLnz7qZ6S+QfX`#Ew5!*_%)j^hF)9klk-w@nvYs%b91Yd1K!akF76oN)&p&pU z)$i?kdxKp=f@;FMRi%vg=gVXShKEOrHM0CQwp$Fe|+%;8s3yDG>cLe6ugqUH@DErp5yQWeUB7i$R z&c*dnK&HH5uI8<@iH7-ihk&FHDT5!8>3D1^@E7-10KN(}`;^Do89Tm5 z_Sk=KW|TK$8M%kSzfNrRJ|BDNTvlla7|&hn)3DSXGoUAdoWFHvzn2^MOmmSU5%{+x z({&vktdDMSSvE>b6CDA@mvaFwh*DkcP`Oi|8N#uLcrBH9Qm*C%nez8ZkpsIffy7co z>%Q>0Ux}ffsWsR)UcM%0GsUCV03rrjhm1Vuyhv4Okn=@)d~QZbOQgso*ZX`CW-Bb~ zRJSRcZ7BxQOU>E2R~-`Ryf@R+{1^Q>;-|}Bo^QMV0Ko`M>usy`k^uo;LVu6E`7J>lYgSh(w2)aS!j&O?tS9k$0G$;Ygw1<2=x;(1>OJf213#B3=KG!f6i)6ZYf=*@B!ApZIBVmwR2hS5mB}QM zNd%WU%A{zMsn}#XfpX7Y0j7e<`EAhN$C4$!a}U^BUGm&L&Jh3N;zxhfHDDby5iE!S3p`^XwUKXs2cIpBv~C8hlOwIiNf6SvV3p z_(YrmfbuEio0)BEpbU{DG^^P#{@fI=MyS9>h-co%GMRi{%VDe3A3w5_lapti`@^vO zsK4&#?`Mg6zM2)7mEdW4=Z~ID4aC?fx9{CcP=c1*qgYy>W6O7HTnsX1Z>qD_(@y9m zRqNGns{$r3D|M@*aVdqW+~&$O_e=xf*OERrOSXSxB~hN$0MH83TvX=hdSm2~yc1Z@ zkiaLh<>aX2gfFix#TR(Px5H>v*sidYNH1MJr~x-^ zQTl~A){Mf#!;t}M#3?Q$ZXdp5*Ph|gfFv4R);h8!@b@+=NvJURlh!SGp@V(y2zC)I z5w90rY<(b(4y#w8R_0)c8nMmn!i?pWK3?m*{Zyy`dA8f?51;lsZD)D7npLNv5_U1d zOmUkY&lIlRbAKWsT){lW7!wh4N}jwS8}v!^@f;~dD-sbpPQrk1lU@4~4XcTeo$*-= zM!p+O;fxW>{&13F=rW!2^{{-O0U}Y*cX1+326Y%p^>5LM%y>l%c|4oilj@h4ZOb;z zkHlfZSEsf|8S#%4ff((_5Xx$5PM`i@|McFa>wI{BaWV)G10qiXzO6<{w_X{C9hM+is&9T#qe}AnJQo?P9xHlb;j2y!{dfYio0Y zZ*by9Qp{Jy7{6Jj7i#eExU&YUDha-+j1|bYn^eF%0r@m`Q8BAiDa@OoIUurud6K)xG_u8OlHY>o9~vzvGq4ZufBkB zJe_|}T_e0nI`ozO%NECFoBZETxDpEdky#_)H@1HH@7%$!J;?Hk2Ru*61vJ*qY z(-a$uWN4U-RA)xE31NjnY34n7d7IpZtuFJg@Tnq60Zpr`M$UJzt;u?`5U2rikMoSD zq?a1hGEUw{f~+%E%_V$$&@B1$%omqF8h#Aq9=gbaf`Xocpoithc>V0 z$B!TOZ{NQ4LH~dN$Q~#YvBX|9F^6GHxyB^XOBi=f;X#O1&&-oDxpj$_TcI2s-v^9h z0kqiv>YZR2t2YtlPjQ8PHSqSn!8=;0j#>8@<($61+;g;q#)BpK`7ep6BY3T5Y-u*h z1o2pN&Nf6`rYBLDV0wfgNC&n&G_UAr-`Ytv;CT#%>lGI!R~}=TknoQ6ai(jxXouZ& z0V~JO;h!pfPvjL$DNnbT<3?H!{yNudce^nFfehHC<+98^2-N9=Pyp6o3nIv4`3e_c z6y3_n@?^Q$@?4i10tIq%aKwTh_6v6oQakZzb8r&JnfM2#%(3VuQ2C=!9(8G;6SW$P z^vrnbTro;OUTT12q}@<7iLsE=BJ8kAzp-H88{X{20+S>YHwp4vX-1_(T7wcdY z_ZI8pTqE375QUdzp6Ia15akLHlsFm~!cjiO!kC@t1N)t7I5W8CwAAnqrA&loYrfU% zXyG6|J>5@AwI|OUwew^Ov>3o?BSy{6*JEv>DxW;A$x8HImDVwn^18mCk&#y9rTq=SAy7Z`SWx;rO8aBVn?S_X0Wzf2+D3#(alwCEjDfKw$v5U&>s`NW1+3CZv` z5{ht+F+$XG6Dw>)jte!N00B_qKD~>03c*7G|42;jTw_o!w}kic;;$WuB(-t91v*Gk zj#=b}{j*5wD(-O%$dPhP@$35D`_r(z_L&~7d^=L*ytKicfI|yYBZnbv+-L+j|D(0HD(Ztg~s6L*Jeou1_nO!{;*$9_m`u;M@M^P^OPC?3HN|b z-^JIDy4!JYiBITNXkq7avF+op{AWC0qYY2^9F~_hv^Xshy%BLU6JTLsK>RaSPk|sVUL7eMdt(O|Cd(s3Qb>9W}x*+6r`swKj zj94*`m1`0(rDTdH-Y7`zDl~)_7FxqD5U1_!cy zh~4G~yynoWb3T|UmL%RCc7k-2?Bwa`UvJ4IL}OZ(*~?_H44eID$FXEbfXUiaun{@> z=wD(i_N#5~vXzyUs0c3;MNCRcgG{JKF6c1VT2Dz_O!2nY$tu&rReLy&Z<^ZmuYEhx1g$471L&t?x-2e-Yx0F#Q8sKm2Q z1{A*C$eobM7QU!+%zM=pO;)k61i6JUxt@bib%%^)`nyKv%iZxu%nE>Ex3#TpkD%|x z-;3Ho>_NymX?LDP(^)8;_2TUhTOz@skE>XZSz;+DC09Np!-uNRMzf)I>C9h$;q8 z0st)|unb&^&D6}pG9@0XZ9#$$~0#;)S-H|`YH$Rvtbd>l!_^yeMriUr2T zsi*V(BVb};^7HWYTxzV9mdFi_jO`?Vk;Fz}#7_QT7QjH@f_Cf(cDZIpEgXRBp{bH35HAuf86*W4ND_)s)N1ey4hFXf`o^)zasfp?)Ryl3{fleZ?qLm(BQ|bx z%YSt<(qKL6ViSzELG7isDdZLl(aMbJ)0LYO0PHI6<>zK#4@C_zZ|QwpiRDGM&+y)A zbKkk?_25iOObqVn4pOJBUhM`yhLgz-b5nno!~GWd%bI?orHH;?=mFvNv-Ae7c7i6$ zWENlhujYPFPZy=<=AP{FzxQCyd#hDq$K8FmZP$W~vaKO8Qvo2z0P+(&dao4lCTp}o zs$`*g8cjIrJck)bl0aN9-&`i1$bZ}B1K*ri8f^(W&+sH)-m02tz%>+ z4Rwv_V7^Nm{Li6emufV{Bytk(qdeQae)e%=$7H2so6-l+ zXpOsx&sjx+?J_Hn;uE=T#HrGAchg#!Py)oIy%pV}Japm%-seV3HH?y;cQ`P>KUTtp zXA%k;k(C%#evu)YQ|7w9Lr4jjyXUFl2H|HNxw~K}Ad%;(nPL@V5&Uma+}L3_{anG8 zMAZHz(RT8?q`QO1IFI|KTt`~7b0EU2MfiF9#Ijp-rBK?5eY{Y|QNKc7b-RDxzy!rh z)%U{%M^^1={UF8$HEowgXlD&HmYm;ewnUASgoI?dNGaLvzGOUC^5q{Y27Xq6Kd zD9zcr4s|;{XzJHc!oA1>uN~#%dxqOjGLyekVzRn7^*$NH)XwnXd6hx{ulWbTPk07F zCi;*3wrZOsWZZe>(OQJcTwH(yUz~W&WK9hXShf9Z2?@fy3-fvIs&*Yo4M26P4VoQ< z!Uv@u6PbDAHThqW4ea!{A1{W1#9dy0M*@uAa=!Z`ru#L;!M1pz)hJAbzl@9hUECIf zy?6lyrYs<4X|)+SySbiz{*|;@u=?Xd%lc^*j*)VvPL{G1AMbg>5M%LtD*`*zb=r>L zz3eYdEIB?h5iLTt6IlyMNlA`x<*=O;hzV89`fM4SinE(+#xpj0F90-HV|k)b2Kw1& zfMd_Que#uS23sAK=vH2_X=9pzkrNz;oNoY3MqQC_X2%cRdHD3$p=ltsY8Ud;3eXe# za@@8yGErm%WKOWM)=QW{bB{Y=BLyKB{WCwG61u=-X=9u!<` zfHq`FBqttH&mTFP)%=W!c_3)p+!N+qE|Ol)2vk>PCOhP(!9mJfLzuL;mgmCi`G z<|YM1-X2<357G{ zJk&`n^;vBz>T#GgEVelJU(_%Ikfh=4#mrN}%?(VuTTju+XsS{U6nT>w2?+nr8GcFo zv{3#{lEMgqO02GDQoKvisti4jl76?UfM{W?{c+)qr-SXGm?LJj3~ki;edsE;%w?NC z`tZ}7VBZrU*MRZ?mGMYKUCRn5axbV8gto_eLyFcmf@Wpgu;}CYmb#J%yPV32;!Vr8 zRey#X=7Z1nxw{C)aufK``{tftJsAFUIqACi)9<$3DFlz_kG44x@-43;09M;!-^4kLYo60vn>>Y;-k=qY=@H5r!ovh?oj zADB7igshMSdcUDh3YRfOz};3vgaJaiQJ%Vz$Voz$TMAHm>WH<
1S1Laos6QJ|z7{|e6XW&EQ2+{gsjBUfcg=7v> zlWR*8D&5u_{N{Z554ol~#jYr-JUlzuCWD8e6^-=`4Sdm`Hlw4X`(KdkKpXF6RM^A( zTYg~q^Fk3}PL9IG#YG4r6MOXZ^sJ>9M%}6Q{Hh&rZmihyIGZ8!5B}x-t*=9`}vZl=Q{n#uTA@S?$3WP(RL@1 zN5!4sQ3`SN(M)9Un1{4jjU-L|yj>R$8`k3>Tfxtq!Y77oCxQx;Te1m2cbmfq#}fE3 z#y=TG%5BmWyE^==pmSJy>##8S?Vp;kt+QKBC)a9H3-AmdlW#gQYA<{ch(3r1ya;1y zn}XOjfWHi`;%zA|e>kaYCPvYKjfUa3Qorwa~6Shd`G zr(Th0NV&{@sf}u8W-)iQ>RI>W>7@7R>ir>N*%l%aE>=)ijxqA_Wrw#(py@vM!Fc}G zL0n?>>4QcP*d}z8N|~!NsIBuGkli6zjMc*a!e!w3;Qvoas;OIr6DXl>s z_GBQnyggkr-uN#BJZC4qyW8}mp-DWb!pHTTS6C<#lwZGIK$kVkFjVE^BTnFut|y9u z`z)A*wAw~Z{5VlK-jOHZ?v}m_@mq`AI=B+v-{4MQ{e`@~1A8%iPl7ra-_6;C+P4~Nr z5~&21*U@2E;i^|lSgbiCNsBJsUSnJzY!>v6TH1Um3JsPg+|bw@jU8lZo7LmIb1H;3 z00`z~YEuH-sjO%O>uz!LY=(zz4MgmWJfRD43^fpF$@lR(3`?Dh2p@d2fs2>V_icb4 zJ{~1syFOJm*Oe&-!tx`od{s=RTvwi+r_qubsj*q`COoZF1nX%giPYT0lgCLPTfV7i znDQLDMEc|J33qN9xqNW=d1LrM`u4*sR?Ty{ZewI~Cu{3}gC!*;Q%&}>O?0x~HZdxW zD+q>vw*=?W?9H$XmzRl<&6YAJ99Nx7ZaT-mZa?gQ#1KSp_KNGg)~VDz_<~PSM9N`Y zB3buPoHhsqW__YTY~}Fu3PE-(yCzT4fYhq1t^2^o0#l)Ht@!Onp>#gr`IVya&kJa? zdCO!FW!)^f$Pra8R9HG%n6|dIk2(35qOvkYzy17(rcMM3MPmz37O@fe+h#I$*-=&N zg!0m9&xjVK0T#>fnGZ$3f)VJ+!$691Zu3vdk3WY;e`^1@cAf~mt2IjmeIC)RHU+0K z(V4dB1A+4WwQcTwkswE+(WK>xc$S4#0=sn^Z1Yc^u1qJ{v7>*e@zG)e#uLhy(do_Li0tPPu=X9pVDJ_ zV~H0rF{+{^$z!6n~mKn=)n9FSU)Ou2#>I(4U+zE-QVvvG`iyyukd2 zlH^3O0X=9yYX45hwW?}8JMmAp;4^8KPB8I9E2|hKV-7w}8Dht158)}-<4qmY5W66M zw<2f)_G&s$9}qd6BMO(#-nfVGdy^0*71nAxs|tKo39AOiLA2&|x*(KZ&peDu(9A`J z$-M34_yRwRngxmn$$axTa2qn=xUz80F+bRCw@s!?L69{dZ4F;sBQ`#~2tPgRJb{r^ zEPwV;1gOgWwUFA-tI_Y{^66YmGa- zNp$n8syI$=R$K|z+@WvO*H6S@q^_QfPHI8|yoA0MhU|pupY><(%fyPTYZj~2Lk>gf zWsu*&EE=_Ow~e@{T>N`hUDqH^8_uRSp`Uy#VFKUa>u;tB`g&fc*#8mp$e8C8d@27G zCWF%doA@@H_zYr*NGmaLNV%+pebzlE-#re;QGRKxqXVD}c_LH@~oAC<{tUz{s(74Jq-R{ABX^r*+-fGWMQ;TqzH&8HH&uE|D*j(&Yg zvfb4Z>c}!AC$zyzKsO)ZxzK|?0C`V_pvI`3FK7c8@*D6*z-}tO^G}!9xa_CpG$~r? zY9%&95HDR~6n%gDL38zTLchmPvuS#GJ(T9ppDM%B$zK!GuBI>X_LRBrriL0+Lu{D%r1%jr5o2U4h}a@rBV4x|){i6o zR5O9tAuQC|8v~!@pU_Y%4t#M|vS_d(SuQwt2&r9-@We>zsf|k)iZC9gerFSFqT4N> z`{PsDM)P$cnLC*?kGr^T@|#a9&Vm#7dAA#-;Q}$ zy%&AJp=7^GtOSdh=p{H~s2Np^49(a$t2CMYTA>0@>QF8oR$)nmOhy(;s^{;MF%m7c zs#sP4?vorG9G)K#PQItn=X`sLe^TOdscDG2>%nXs+85xqeOA$+?%~pa1`RN%HtlhQ zXD(arlpb_vcV5-c+8kXjI87_KF6M94pER~z`ZDd%6G378lK8p%Oymq6afX0~muJD&w6nSkXkLws8T z%iAjr)s#4xyHt_V?pmGUa+GO5m2lqm0S`~9m*$T<Ae?qs$ngz;{C`eKga#j{PW zv%_bjb;uHNK18TZqs);gxp{4B3RbzgtrMtPiPv@1NAp9bPfxBlyie7xv6#C1BZq4Y zoUAWlOIBFIXCs@tZDXD+Yp(SWcI`59>$FMsBZ!zq_^#3$)nDPg52dW8)BuxEOr9(w z3bFb_OeB;oM*|wpGUlI*_XS-GLDM?3Go(GXs2GHefkcoBnP@HYC-|EeKr1)#=6q=O zogtg5#DM5J^Ai)g|D0AHeKW|-vDOA2(WY9Mtw@NDW+SEme8e77Qe)$&?h-F>^+1lY zF_JF)J;;vtn9<5j7VL{5p*FpCHx;)Is%Cedk(1#4HQZZTk+eORZ+l>9+kV8revX~a zN-c22WGDQ5rRYo>ZTjm{=$mb^ZtGW<3;T^ywV@!Ct@kzFnkXN$Jc<@x%`ROcqt%mr zBUtOD>2$yhJg6Zcxh==Ka$i8_QoCxmW2BwstA0l-v^x=M^279*r&(yzTlbNhE>*_e zBM8#qv2bU&G!u&h#pqwkNAJA_fjh}S){L!H?h@ULiH7(QfFRZ(yprmWUh@!XQ zx+~g~B;H{9S@Q3;#^J@|6BvdR2|c?;FF1Ku4*d#wgZ#ACKrxP=4m^`oWK zPk$eVw}O&(r^&O92C74L2=tq7M6Vd)T^j638#;pv4jqQVwhJ=xn z@mBV;EoC!K=a2gcMpx`EKl$Pk_*YMjPJh{$1-#?KaZZYWM1mf_FlOd=!j*MtWQ-_; zkLS3S@6<9*opD=b0=<;Ijs#sK)RQ7j5S$aN?CE=un#xk^di|N5fb&?F;;zllSMjKY z*9LY#YPbh?ZRF{GOTFNfvzFA6?y>{sFk7Bg-BD{pZ6YOo5>r>UnHZ6yAj9jFggf7) zQjq`D8rgZ@#c4T6Fd|7u{-ZyqzwI^Y#?R02MB8NHS2tc8udlzb zw-caWNoE>ceU_HmP8wfM#d?1X<(K6Vf(wOS*1s|MH49kIHCW@rl7tjUYUPVaS1p2f zi;v)`L1qt(spu(RR{pvkbTe602&~H0=eZL`b&vLbb&jyk`sX3YwFV<@5X%`pJKGxp*J=oux+y7m*9`Zs~2GJFMav9^ia+ zA7hKX8KjRPedPT{Qp-z(^f-5dQ+;Ge`b_!V)QeZz{ps`C)N+rZK8%($m=#GpIZA$4 z_R*5uWJZYPvjW;|qTcl48^Cp6BxN8`v8X{30^B~e%TmH#Bp=*m?uUL)2Cn}!TCrC1 zpC%%SaXQ8>llj^5xT5Cc!q*oGzlnu2Vn;%aY3$vE!>A7oDIYvHl;NFMdu+rkIe+o0 zeb;{XGZVtR{cPfzu`#)sjcoOY&totf9A-58+y>4>eKQ%V>x>Ko9Iq2Mw$&dh9SZLZ z?*1(=NP2Bh*z8Fc=nwUR5%V?Yz}~M=z@vC&aZ+NO`K`5M;qZ9_H0=?3OQzw zKZtXX-n#q7w9{X_pyIfU^mPx%941b=Y>Hnsk%;;t!R1@ViwFnAkTd*C*VcGHeQp`% z1y-I5JtW!SF;*cU*f$CIOl~Wv&!ntvbeDgaJ9<#BZ-s^R4Og;NDU>N=mQ(_lJ_E8R zSl+xGm=8OEkwm+u;QVg#Oj3cr?B=2~vEipD6oKl9yC^Vp1{v|i>CBCcMZVvByy8#g zp32(3+z7lsy(93@X6+VbyPi!RE%dY+6{(EcPKX*3`?lTg1YW@_ac3t$v?!Csm+0MB zLJI8p+p5P}o_EV~G_}PV{)I*@Zxd#mwO|Q4Xcj#T%+Wa9%um#kz$8qlam3^beqqo} zduG4ZM@RUd>k1cmxXJ7hEt z_ds>tTvaF+iZqiK=Xx)FV`%Jjc6dAt(V_JYA!Lt3BprWeYu2bEeJB`meNCeuq(t~X zIvPGp`F&#j0OnfT@7cOgK2dSJh+ziCsPe3KxQ1O;#08ur72Gzd*a$s0+8&g)mY@A) z6=M;7ARnQG*M4EYrXi6+nU}X&6OMrCyqr~KKP-iMG;#gpZ!TKb>iJwW_P}(amcQ?B z)B%D49{IB6h>676nDq*nT#*d@6d(W1fR`bB%FX;)KtX66c}C%_z}nXD8LET(jDhS- z>CTU=Vl<>AG*1B@`ukE}^0D)mxaNh>oBn#4I*^ifRU7lP7>S>qqA%OG#!piT$1PBS zOC0e37;%p9n||@}6i_gz*@f+%cwYS$t=V%&oDL8iu?|LG6VB{OciEV!xMp zRI&;8K0TSe;`akpv#i0IcK5Ni5BTL(JSJ?m-F<};%6SLSC#mc5+il01D=^RDjTysfu<=bpRF@?@6^jxTg{A}lyhfNypOhg$V}~x zUf$0Pn2LJC^7+`+-dDfx=}tc6?^|7Ed37^z*5c!`zcL9~(5FP-3OU@)Dir@H#j%>^ zW_GYU#>c&p7S!g3rZW2SYir^W_#d*ue0CvjSB7#*>Z9BPF#lJv#u+7^ly!{U9P_<{ zdY;#Gvbza-2`>O9B?aPPvf8Nyq>C(*;HDEs?CIuYsnwBQqrH)5DV{94 zq*{L}Y4Lm=g-KM`Ru6yu#=WgE+1?df-gaPiXiHxtWDS!Ih?J&7`-U*JFi?7MYeG{v zjZKWV=KhcP%$Bv)aVSu}9CeB-*gRiV7cNtZ53T z5i#=pGrLY4{UMU&OA*6QS}T&^MTjk*`x_Dsm%+r8m)L=NnRkkE(@O3Wr@Q;6SH-L- zF3h}`pn94=u*-t0aJ+a!{(0`MK9LRbOmL4&;Mk_#8I__zLEj*tRi=-yj6 zHseU(TvdpYp46SWv%x9ff8U3EI(gG*|4qPIX4M6wi92Jx{k^%u?<|_B3B>Cte(eL+ z#aiA%hk!9UMkhlJLIG=#z;0%5v?0Sm$H5WC5$dWA1z-0QT1ib+pb; z3O9nv-dD3-W6Xx=lYvRhKxQDSvs(Tg!4UtUYJ_+x(nO+x_581v$(+7Ap3Ig44OXh0 z)yiIFY1)#N;3^q0hx^=yGy1p0*vOMlD^xk?KqvFBOF(85}AeLZX|&#>MSM{SRY{n^S0Odd$qiufWT@<6g6nzfWVg*cU~ zm Date: Thu, 20 Dec 2018 15:30:17 +0100 Subject: [PATCH 017/384] fix readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 8c063448..a42cf433 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ [![pub package](https://img.shields.io/pub/v/flutter_hooks.svg)](https://pub.dartlang.org/packages/flutter_hooks) -![alt text](https://raw.githubusercontent.com/rrousselGit/flutter_hook/master/hooks.png) +![alt text](https://raw.githubusercontent.com/rrousselGit/flutter_hooks/master/hooks.png) # Flutter Hooks From 1f58f2e65f6cf78f5d1bc6644d3d715b25bea82e Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Thu, 20 Dec 2018 16:33:39 +0100 Subject: [PATCH 018/384] test setState/context --- test/hook_widget_test.dart | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/test/hook_widget_test.dart b/test/hook_widget_test.dart index 456a37d1..9a55d116 100644 --- a/test/hook_widget_test.dart +++ b/test/hook_widget_test.dart @@ -27,6 +27,30 @@ void main() { reset(didUpdateHook); }); + testWidgets('hook & setState', (tester) async { + final setState = Func0(); + final hook = MyHook(); + HookElement hookContext; + MyHookState state; + + await tester.pumpWidget(HookBuilder( + builder: (context) { + hookContext = context as HookElement; + state = context.use(hook); + return Container(); + }, + )); + + expect(state.hook, hook); + expect(state.context, hookContext); + expect(hookContext.dirty, false); + + state.setState(setState.call); + verify(setState.call()).called(1); + + expect(hookContext.dirty, true); + }); + testWidgets('life-cycles in order', (tester) async { int result; HookTest previousHook; @@ -438,3 +462,15 @@ void main() { verifyZeroInteractions(didUpdateHook4); }); } + +class MyHook extends Hook { + @override + MyHookState createState() => MyHookState(); +} + +class MyHookState extends HookState { + @override + MyHookState build(HookContext context) { + return this; + } +} From cf9b45fa563856ca02879a82f4a551597de08fdd Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Thu, 20 Dec 2018 16:49:39 +0100 Subject: [PATCH 019/384] useState test --- test/use_state_test.dart | 74 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 test/use_state_test.dart diff --git a/test/use_state_test.dart b/test/use_state_test.dart new file mode 100644 index 00000000..b5d830e1 --- /dev/null +++ b/test/use_state_test.dart @@ -0,0 +1,74 @@ +import 'package:flutter/widgets.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; + +import 'mock.dart'; + +void main() { + testWidgets('useState basic', (tester) async { + ValueNotifier state; + HookElement element; + + await tester.pumpWidget(HookBuilder( + builder: (context) { + element = context as HookElement; + state = context.useState(initialData: 42); + return Container(); + }, + )); + + expect(state.value, 42); + expect(element.dirty, false); + + await tester.pump(); + + expect(state.value, 42); + expect(element.dirty, false); + + state.value++; + expect(element.dirty, true); + await tester.pump(); + + expect(state.value, 43); + expect(element.dirty, false); + + // dispose + await tester.pumpWidget(const SizedBox()); + + // ignore: invalid_use_of_protected_member + expect(() => state.hasListeners, throwsFlutterError); + }); + + testWidgets('no initial data', (tester) async { + ValueNotifier state; + HookElement element; + + await tester.pumpWidget(HookBuilder( + builder: (context) { + element = context as HookElement; + state = context.useState(); + return Container(); + }, + )); + + expect(state.value, null); + expect(element.dirty, false); + + await tester.pump(); + + expect(state.value, null); + expect(element.dirty, false); + + state.value = 43; + expect(element.dirty, true); + await tester.pump(); + + expect(state.value, 43); + expect(element.dirty, false); + + // dispose + await tester.pumpWidget(const SizedBox()); + + // ignore: invalid_use_of_protected_member + expect(() => state.hasListeners, throwsFlutterError); + }); +} From 54f261d3f34f6a0e43897e235f76070cbf24b759 Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Thu, 20 Dec 2018 17:14:02 +0100 Subject: [PATCH 020/384] test valueChanged --- test/mock.dart | 9 +++- test/use_value_changed_test.dart | 77 ++++++++++++++++++++++++++++++++ 2 files changed, 85 insertions(+), 1 deletion(-) create mode 100644 test/use_value_changed_test.dart diff --git a/test/mock.dart b/test/mock.dart index 712e00f5..d28a056c 100644 --- a/test/mock.dart +++ b/test/mock.dart @@ -3,7 +3,8 @@ import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/mockito.dart'; -export 'package:flutter_test/flutter_test.dart' hide Func0, Func1; +export 'package:flutter_test/flutter_test.dart' + hide Func0, Func1, Func2, Func3, Func4, Func5, Func6; export 'package:mockito/mockito.dart'; abstract class _Func0 { @@ -18,6 +19,12 @@ abstract class _Func1 { class Func1 extends Mock implements _Func1 {} +abstract class _Func2 { + R call(T1 value, T2 value2); +} + +class Func2 extends Mock implements _Func2 {} + class HookTest extends Hook { final R Function(HookContext context) build; final void Function() dispose; diff --git a/test/use_value_changed_test.dart b/test/use_value_changed_test.dart new file mode 100644 index 00000000..747fb5a9 --- /dev/null +++ b/test/use_value_changed_test.dart @@ -0,0 +1,77 @@ +import 'package:flutter/widgets.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; + +import 'mock.dart'; + +void main() { + testWidgets('useValueChanged basic', (tester) async { + var value = 42; + final useValueChanged = Func2(); + String result; + + pump() => tester.pumpWidget(HookBuilder( + builder: (context) { + result = context.useValueChanged(value, useValueChanged.call); + return Container(); + }, + )); + await pump(); + + final HookElement context = find.byType(HookBuilder).evaluate().first; + + expect(result, null); + verifyNoMoreInteractions(useValueChanged); + expect(context.dirty, false); + + await pump(); + + expect(result, null); + verifyNoMoreInteractions(useValueChanged); + expect(context.dirty, false); + + value++; + when(useValueChanged.call(any, any)).thenReturn('Hello'); + await pump(); + + verify(useValueChanged.call(42, null)); + expect(result, 'Hello'); + verifyNoMoreInteractions(useValueChanged); + expect(context.dirty, false); + + await pump(); + + expect(result, 'Hello'); + verifyNoMoreInteractions(useValueChanged); + expect(context.dirty, false); + + value++; + when(useValueChanged.call(any, any)).thenReturn('Foo'); + await pump(); + + expect(result, 'Foo'); + verify(useValueChanged.call(43, 'Hello')); + verifyNoMoreInteractions(useValueChanged); + expect(context.dirty, false); + + await pump(); + + expect(result, 'Foo'); + verifyNoMoreInteractions(useValueChanged); + expect(context.dirty, false); + + // dispose + await tester.pumpWidget(const SizedBox()); + }); + + testWidgets('valueChanged required', (tester) async { + await expectPump( + () => tester.pumpWidget(HookBuilder( + builder: (context) { + context.useValueChanged(42, null); + return Container(); + }, + )), + throwsAssertionError, + ); + }); +} From 29f1f2068081423fbd22b8d3cbde53b601df6789 Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Fri, 21 Dec 2018 12:23:04 +0100 Subject: [PATCH 021/384] TickerProvider and AnimationController (#4) --- lib/src/hook.dart | 1 + lib/src/hook_impl.dart | 116 +++++++++++++++++++++++- lib/src/hook_widget.dart | 59 +++++++++++- test/hook_widget_test.dart | 13 +-- test/memoized_test.dart | 95 ++++++++++--------- test/use_animation_controller_test.dart | 108 ++++++++++++++++++++++ test/use_ticker_provider_test.dart | 63 +++++++++++++ test/use_value_changed_test.dart | 17 ++-- 8 files changed, 405 insertions(+), 67 deletions(-) create mode 100644 test/use_animation_controller_test.dart create mode 100644 test/use_ticker_provider_test.dart diff --git a/lib/src/hook.dart b/lib/src/hook.dart index bd681bd0..fd0249b6 100644 --- a/lib/src/hook.dart +++ b/lib/src/hook.dart @@ -1,5 +1,6 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/scheduler.dart'; import 'package:flutter/widgets.dart'; part 'hook_widget.dart'; diff --git a/lib/src/hook_impl.dart b/lib/src/hook_impl.dart index 23ff13a3..2d958efb 100644 --- a/lib/src/hook_impl.dart +++ b/lib/src/hook_impl.dart @@ -1,7 +1,7 @@ part of 'hook.dart'; class _MemoizedHook extends Hook { - final T Function() valueBuilder; + final T Function(T old) valueBuilder; final List parameters; const _MemoizedHook(this.valueBuilder, {this.parameters = const []}) @@ -18,7 +18,7 @@ class _MemoizedHookState extends HookState> { @override void initHook() { super.initHook(); - value = hook.valueBuilder(); + value = hook.valueBuilder(null); } @override @@ -27,7 +27,7 @@ class _MemoizedHookState extends HookState> { if (hook.parameters != oldHook.parameters && (hook.parameters.length != oldHook.parameters.length || _hasDiffWith(oldHook.parameters))) { - value = hook.valueBuilder(); + value = hook.valueBuilder(value); } } @@ -109,6 +109,116 @@ class _StateHookState extends HookState, _StateHook> { } } +class _TickerProviderHook extends Hook { + const _TickerProviderHook(); + + @override + _TickerProviderHookState createState() => _TickerProviderHookState(); +} + +class _TickerProviderHookState + extends HookState + implements TickerProvider { + Ticker _ticker; + + @override + Ticker createTicker(TickerCallback onTick) { + assert(() { + if (_ticker == null) return true; + throw FlutterError( + '${context.widget.runtimeType} attempted to use a useSingleTickerProvider multiple times.\n' + 'A SingleTickerProviderStateMixin can only be used as a TickerProvider once. If a ' + 'TickerProvider is used for multiple AnimationController objects, or if it is passed to other ' + 'objects and those objects might use it more than one time in total, then instead of ' + 'using useSingleTickerProvider, use a regular useTickerProvider.'); + }()); + _ticker = Ticker(onTick, debugLabel: 'created by $context'); + return _ticker; + } + + @override + void dispose() { + assert(() { + if (_ticker == null || !_ticker.isActive) return true; + throw FlutterError( + 'useSingleTickerProvider created a Ticker, but at the time ' + 'dispose() was called on the Hook, that Ticker was still active. Tickers used ' + ' by AnimationControllers should be disposed by calling dispose() on ' + ' the AnimationController itself. Otherwise, the ticker will leak.\n'); + }()); + super.dispose(); + } + + @override + TickerProvider build(HookContext context) { + if (_ticker != null) _ticker.muted = !TickerMode.of(context); + return this; + } +} + +class _AnimationControllerHook extends Hook { + final Duration duration; + final String debugLabel; + final double initialValue; + final double lowerBound; + final double upperBound; + final TickerProvider vsync; + final AnimationBehavior animationBehavior; + + const _AnimationControllerHook({ + this.duration, + this.debugLabel, + this.initialValue, + this.lowerBound, + this.upperBound, + this.vsync, + this.animationBehavior, + }); + + @override + _AnimationControllerHookState createState() => + _AnimationControllerHookState(); +} + +class _AnimationControllerHookState + extends HookState { + AnimationController _animationController; + + @override + AnimationController build(HookContext context) { + final vsync = hook.vsync ?? context.useSingleTickerProvider(); + + _animationController ??= AnimationController( + vsync: vsync, + duration: hook.duration, + debugLabel: hook.debugLabel, + lowerBound: hook.lowerBound, + upperBound: hook.upperBound, + animationBehavior: hook.animationBehavior, + value: hook.initialValue, + ); + + context + ..useValueChanged(hook.vsync, resync) + ..useValueChanged(hook.duration, duration); + return _animationController; + } + + void resync(_, __) { + _animationController.resync(hook.vsync); + } + + void duration(_, __) { + _animationController.duration = hook.duration; + } + + @override + void dispose() { + super.dispose(); + _animationController.dispose(); + } +} + /// A [HookWidget] that defer its [HookWidget.build] to a callback class HookBuilder extends HookWidget { /// The callback used by [HookBuilder] to create a widget. diff --git a/lib/src/hook_widget.dart b/lib/src/hook_widget.dart index d03f8e6e..02054a99 100644 --- a/lib/src/hook_widget.dart +++ b/lib/src/hook_widget.dart @@ -332,7 +332,8 @@ This may happen if the call to `use` is made under some condition. } @override - T useMemoized(T Function() valueBuilder, {List parameters = const []}) { + T useMemoized(T Function(T previousValue) valueBuilder, + {List parameters = const []}) { return use(_MemoizedHook( valueBuilder, parameters: parameters, @@ -343,6 +344,32 @@ This may happen if the call to `use` is made under some condition. R useValueChanged(T value, R valueChange(T oldValue, R oldResult)) { return use(_ValueChangedHook(value, valueChange)); } + + @override + AnimationController useAnimationController({ + Duration duration, + String debugLabel, + double initialValue = 0, + double lowerBound = 0, + double upperBound = 1, + TickerProvider vsync, + AnimationBehavior animationBehavior = AnimationBehavior.normal, + }) { + return use(_AnimationControllerHook( + duration: duration, + debugLabel: debugLabel, + initialValue: initialValue, + lowerBound: lowerBound, + upperBound: upperBound, + vsync: vsync, + animationBehavior: animationBehavior, + )); + } + + @override + TickerProvider useSingleTickerProvider() { + return use(const _TickerProviderHook()); + } } /// A [Widget] that can use [Hook] @@ -424,11 +451,39 @@ abstract class HookContext extends BuildContext { /// * [parameters] can be use to specify a list of objects for [useMemoized] to watch. /// So that whenever [operator==] fails on any parameter or if the length of [parameters] changes, /// [valueBuilder] is called again. - T useMemoized(T valueBuilder(), {List parameters}); + T useMemoized(T valueBuilder(T previousValue), {List parameters}); /// Watches a value. /// /// Whenever [useValueChanged] is called with a diffent [value], calls [valueChange]. /// The value returned by [useValueChanged] is the latest returned value of [valueChange] or `null`. R useValueChanged(T value, R valueChange(T oldValue, R oldResult)); + + /// Creates a single usage [TickerProvider]. + /// + /// See also: + /// * [SingleTickerProviderStateMixin] + TickerProvider useSingleTickerProvider(); + + /// Creates an [AnimationController] automatically disposed. + /// + /// If no [vsync] is provided, the [TickerProvider] is implicitly obtained using [useSingleTickerProvider]. + /// If a [vsync] is specified, changing the instance of [vsync] will result in a call to [AnimationController.resync]. + /// It is not possible to switch between implicit and explicit [vsync]. + /// + /// Changing the [duration] parameter automatically updates [AnimationController.duration]. + /// + /// [initialValue], [lowerBound], [upperBound] and [debugLabel] are ignored after the first call. + /// + /// See also: + /// * [AnimationController] + AnimationController useAnimationController({ + Duration duration, + String debugLabel, + double initialValue = 0, + double lowerBound = 0, + double upperBound = 1, + TickerProvider vsync, + AnimationBehavior animationBehavior = AnimationBehavior.normal, + }); } diff --git a/test/hook_widget_test.dart b/test/hook_widget_test.dart index 9a55d116..60665e29 100644 --- a/test/hook_widget_test.dart +++ b/test/hook_widget_test.dart @@ -119,10 +119,9 @@ void main() { await tester.pumpWidget(HookBuilder(builder: builder.call)); when(dispose.call()).thenThrow(24); - await expectPump( - () => tester.pumpWidget(const SizedBox()), - throwsA(24), - ); + await tester.pumpWidget(const SizedBox()); + + expect(tester.takeException(), 24); verifyInOrder([ dispose.call(), @@ -190,10 +189,8 @@ void main() { return Container(); }); - await expectPump( - () => tester.pumpWidget(HookBuilder(builder: builder.call)), - throwsAssertionError, - ); + await tester.pumpWidget(HookBuilder(builder: builder.call)); + expect(tester.takeException(), isAssertionError); }); testWidgets('rebuild removed hooks crash', (tester) async { diff --git a/test/memoized_test.dart b/test/memoized_test.dart index 9cedbdf0..cf8eda61 100644 --- a/test/memoized_test.dart +++ b/test/memoized_test.dart @@ -6,7 +6,7 @@ import 'mock.dart'; void main() { final builder = Func1(); final parameterBuilder = Func0(); - final valueBuilder = Func0(); + final valueBuilder = Func1(); tearDown(() { reset(builder); @@ -15,28 +15,24 @@ void main() { }); testWidgets('invalid parameters', (tester) async { - await expectPump( - () => tester.pumpWidget(HookBuilder(builder: (context) { - context.useMemoized(null); - return Container(); - })), - throwsAssertionError, - ); - - await expectPump( - () => tester.pumpWidget(HookBuilder(builder: (context) { - context.useMemoized(() {}, parameters: null); - return Container(); - })), - throwsAssertionError, - ); + await tester.pumpWidget(HookBuilder(builder: (context) { + context.useMemoized(null); + return Container(); + })); + expect(tester.takeException(), isAssertionError); + + await tester.pumpWidget(HookBuilder(builder: (context) { + context.useMemoized((_) {}, parameters: null); + return Container(); + })); + expect(tester.takeException(), isAssertionError); }); testWidgets('memoized without parameter calls valueBuilder once', (tester) async { int result; - when(valueBuilder.call()).thenReturn(42); + when(valueBuilder.call(null)).thenReturn(42); when(builder.call(any)).thenAnswer((invocation) { final HookContext context = invocation.positionalArguments.single; @@ -46,17 +42,18 @@ void main() { await tester.pumpWidget(HookBuilder(builder: builder.call)); - verify(valueBuilder.call()).called(1); + verify(valueBuilder.call(null)).called(1); + verifyNoMoreInteractions(valueBuilder); expect(result, 42); await tester.pumpWidget(HookBuilder(builder: builder.call)); - verifyNever(valueBuilder.call()); + verifyNoMoreInteractions(valueBuilder); expect(result, 42); await tester.pumpWidget(const SizedBox()); - verifyNever(valueBuilder.call()); + verifyNoMoreInteractions(valueBuilder); }); testWidgets( @@ -64,7 +61,7 @@ void main() { (tester) async { int result; - when(valueBuilder.call()).thenReturn(0); + when(valueBuilder.call(null)).thenReturn(0); when(parameterBuilder.call()).thenReturn([]); when(builder.call(any)).thenAnswer((invocation) { @@ -76,55 +73,58 @@ void main() { await tester.pumpWidget(HookBuilder(builder: builder.call)); - verify(valueBuilder.call()).called(1); + verify(valueBuilder.call(null)).called(1); + verifyNoMoreInteractions(valueBuilder); expect(result, 0); /* No change */ await tester.pumpWidget(HookBuilder(builder: builder.call)); - verifyNever(valueBuilder.call()); + verifyNoMoreInteractions(valueBuilder); expect(result, 0); /* Add parameter */ when(parameterBuilder.call()).thenReturn(['foo']); - when(valueBuilder.call()).thenReturn(1); + when(valueBuilder.call(0)).thenReturn(1); await tester.pumpWidget(HookBuilder(builder: builder.call)); expect(result, 1); - verify(valueBuilder.call()).called(1); + verify(valueBuilder.call(0)).called(1); + verifyNoMoreInteractions(valueBuilder); /* No change */ await tester.pumpWidget(HookBuilder(builder: builder.call)); - verifyNever(valueBuilder.call()); + verifyNoMoreInteractions(valueBuilder); expect(result, 1); /* Remove parameter */ when(parameterBuilder.call()).thenReturn([]); - when(valueBuilder.call()).thenReturn(2); + when(valueBuilder.call(1)).thenReturn(2); await tester.pumpWidget(HookBuilder(builder: builder.call)); expect(result, 2); - verify(valueBuilder.call()).called(1); + verify(valueBuilder.call(1)).called(1); + verifyNoMoreInteractions(valueBuilder); /* No change */ await tester.pumpWidget(HookBuilder(builder: builder.call)); - verifyNever(valueBuilder.call()); + verifyNoMoreInteractions(valueBuilder); expect(result, 2); /* DISPOSE */ await tester.pumpWidget(const SizedBox()); - verifyNever(valueBuilder.call()); + verifyNoMoreInteractions(valueBuilder); }); testWidgets('memoized parameters compared in order', (tester) async { @@ -137,12 +137,13 @@ void main() { return Container(); }); - when(valueBuilder.call()).thenReturn(0); + when(valueBuilder.call(null)).thenReturn(0); when(parameterBuilder.call()).thenReturn(['foo', 42, 24.0]); await tester.pumpWidget(HookBuilder(builder: builder.call)); - verify(valueBuilder.call()).called(1); + verify(valueBuilder.call(null)).called(1); + verifyNoMoreInteractions(valueBuilder); expect(result, 0); /* Array reference changed but content didn't */ @@ -150,35 +151,38 @@ void main() { when(parameterBuilder.call()).thenReturn(['foo', 42, 24.0]); await tester.pumpWidget(HookBuilder(builder: builder.call)); - verifyNever(valueBuilder.call()); + verifyNoMoreInteractions(valueBuilder); expect(result, 0); /* reoder */ - when(valueBuilder.call()).thenReturn(1); + when(valueBuilder.call(0)).thenReturn(1); when(parameterBuilder.call()).thenReturn([42, 'foo', 24.0]); await tester.pumpWidget(HookBuilder(builder: builder.call)); - verify(valueBuilder.call()).called(1); + verify(valueBuilder.call(0)).called(1); + verifyNoMoreInteractions(valueBuilder); expect(result, 1); - when(valueBuilder.call()).thenReturn(2); + when(valueBuilder.call(1)).thenReturn(2); when(parameterBuilder.call()).thenReturn([42, 24.0, 'foo']); await tester.pumpWidget(HookBuilder(builder: builder.call)); - verify(valueBuilder.call()).called(1); + verify(valueBuilder.call(1)).called(1); + verifyNoMoreInteractions(valueBuilder); expect(result, 2); /* value change */ - when(valueBuilder.call()).thenReturn(3); + when(valueBuilder.call(2)).thenReturn(3); when(parameterBuilder.call()).thenReturn([43, 24.0, 'foo']); await tester.pumpWidget(HookBuilder(builder: builder.call)); - verify(valueBuilder.call()).called(1); + verify(valueBuilder.call(2)).called(1); + verifyNoMoreInteractions(valueBuilder); expect(result, 3); /* Comparison is done using operator== */ @@ -188,14 +192,14 @@ void main() { await tester.pumpWidget(HookBuilder(builder: builder.call)); - verifyNever(valueBuilder.call()); + verifyNoMoreInteractions(valueBuilder); expect(result, 3); /* DISPOSE */ await tester.pumpWidget(const SizedBox()); - verifyNever(valueBuilder.call()); + verifyNoMoreInteractions(valueBuilder); }); testWidgets( @@ -211,12 +215,13 @@ void main() { return Container(); }); - when(valueBuilder.call()).thenReturn(0); + when(valueBuilder.call(null)).thenReturn(0); when(parameterBuilder.call()).thenReturn(parameters); await tester.pumpWidget(HookBuilder(builder: builder.call)); - verify(valueBuilder.call()).called(1); + verify(valueBuilder.call(null)).called(1); + verifyNoMoreInteractions(valueBuilder); expect(result, 0); /* Array content but reference didn't */ @@ -224,12 +229,12 @@ void main() { await tester.pumpWidget(HookBuilder(builder: builder.call)); - verifyNever(valueBuilder.call()); + verifyNoMoreInteractions(valueBuilder); /* DISPOSE */ await tester.pumpWidget(const SizedBox()); - verifyNever(valueBuilder.call()); + verifyNoMoreInteractions(valueBuilder); }); } diff --git a/test/use_animation_controller_test.dart b/test/use_animation_controller_test.dart new file mode 100644 index 00000000..d181f24c --- /dev/null +++ b/test/use_animation_controller_test.dart @@ -0,0 +1,108 @@ +import 'package:flutter/scheduler.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; + +import 'mock.dart'; + +void main() { + testWidgets('useAnimationController basic', (tester) async { + AnimationController controller; + + await tester.pumpWidget( + HookBuilder(builder: (context) { + controller = context.useAnimationController(); + return Container(); + }), + ); + + expect(controller.duration, isNull); + expect(controller.lowerBound, 0); + expect(controller.upperBound, 1); + expect(controller.value, 0); + expect(controller.animationBehavior, AnimationBehavior.normal); + expect(controller.debugLabel, isNull); + + controller.duration = const Duration(seconds: 1); + + // check has a ticker + controller.forward(); + + // dispose + await tester.pumpWidget(const SizedBox()); + }); + + testWidgets('useAnimationController complex', (tester) async { + AnimationController controller; + + TickerProvider provider; + provider = _TickerProvider(); + when(provider.createTicker(any)).thenAnswer((_) { + void Function(Duration) cb = _.positionalArguments[0]; + return tester.createTicker(cb); + }); + + await tester.pumpWidget( + HookBuilder(builder: (context) { + controller = context.useAnimationController( + vsync: provider, + animationBehavior: AnimationBehavior.preserve, + duration: const Duration(seconds: 1), + initialValue: 42, + lowerBound: 24, + upperBound: 84, + debugLabel: 'Foo', + ); + return Container(); + }), + ); + + verify(provider.createTicker(any)).called(1); + verifyNoMoreInteractions(provider); + + // check has a ticker + controller.forward(); + expect(controller.duration, const Duration(seconds: 1)); + expect(controller.lowerBound, 24); + expect(controller.upperBound, 84); + expect(controller.value, 42); + expect(controller.animationBehavior, AnimationBehavior.preserve); + expect(controller.debugLabel, 'Foo'); + + var previousController = controller; + provider = _TickerProvider(); + when(provider.createTicker(any)).thenAnswer((_) { + void Function(Duration) cb = _.positionalArguments[0]; + return tester.createTicker(cb); + }); + + await tester.pumpWidget( + HookBuilder(builder: (context) { + controller = context.useAnimationController( + vsync: provider, + animationBehavior: AnimationBehavior.normal, + duration: const Duration(seconds: 2), + initialValue: 0, + lowerBound: 0, + upperBound: 0, + debugLabel: 'Bar', + ); + return Container(); + }), + ); + + verify(provider.createTicker(any)).called(1); + verifyNoMoreInteractions(provider); + expect(controller, previousController); + expect(controller.duration, const Duration(seconds: 2)); + expect(controller.lowerBound, 24); + expect(controller.upperBound, 84); + expect(controller.value, 42); + expect(controller.animationBehavior, AnimationBehavior.preserve); + expect(controller.debugLabel, 'Foo'); + + // dispose + await tester.pumpWidget(const SizedBox()); + }); +} + +class _TickerProvider extends Mock implements TickerProvider {} diff --git a/test/use_ticker_provider_test.dart b/test/use_ticker_provider_test.dart new file mode 100644 index 00000000..8a57f234 --- /dev/null +++ b/test/use_ticker_provider_test.dart @@ -0,0 +1,63 @@ +import 'package:flutter/scheduler.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_hooks/src/hook.dart'; + +import 'mock.dart'; + +void main() { + testWidgets('useSingleTickerProvider basic', (tester) async { + TickerProvider provider; + + await tester.pumpWidget(TickerMode( + enabled: true, + child: HookBuilder(builder: (context) { + provider = context.useSingleTickerProvider(); + return Container(); + }), + )); + + final animationController = AnimationController( + vsync: provider, duration: const Duration(seconds: 1)) + ..forward(); + + expect(() => AnimationController(vsync: provider), throwsFlutterError); + + animationController.dispose(); + + await tester.pumpWidget(const SizedBox()); + }); + + testWidgets('useSingleTickerProvider unused', (tester) async { + await tester.pumpWidget(HookBuilder(builder: (context) { + context.useSingleTickerProvider(); + return Container(); + })); + + await tester.pumpWidget(const SizedBox()); + }); + + testWidgets('useSingleTickerProvider still active', (tester) async { + TickerProvider provider; + + await tester.pumpWidget(TickerMode( + enabled: true, + child: HookBuilder(builder: (context) { + provider = context.useSingleTickerProvider(); + return Container(); + }), + )); + + final animationController = AnimationController( + vsync: provider, duration: const Duration(seconds: 1)); + + try { + animationController.forward(); + await expectPump( + () => tester.pumpWidget(const SizedBox()), + throwsFlutterError, + ); + } finally { + animationController.dispose(); + } + }); +} diff --git a/test/use_value_changed_test.dart b/test/use_value_changed_test.dart index 747fb5a9..9fc0d7c5 100644 --- a/test/use_value_changed_test.dart +++ b/test/use_value_changed_test.dart @@ -64,14 +64,13 @@ void main() { }); testWidgets('valueChanged required', (tester) async { - await expectPump( - () => tester.pumpWidget(HookBuilder( - builder: (context) { - context.useValueChanged(42, null); - return Container(); - }, - )), - throwsAssertionError, - ); + await tester.pumpWidget(HookBuilder( + builder: (context) { + context.useValueChanged(42, null); + return Container(); + }, + )); + + expect(tester.takeException(), isAssertionError); }); } From 22882adee3de34bb0226d4cae4302892a7d2e209 Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Fri, 21 Dec 2018 13:42:18 +0100 Subject: [PATCH 022/384] bump version --- CHANGELOG.md | 9 ++++++++- example/pubspec.lock | 2 +- pubspec.yaml | 2 +- test/use_animation_controller_test.dart | 8 ++++---- 4 files changed, 14 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0df12e43..14032fab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +## 0.0.1: + +Added a few common hooks: + +- `useAnimationController` +- `useSingleTickerProvider` + ## 0.0.0: -- initial release \ No newline at end of file +- initial release diff --git a/example/pubspec.lock b/example/pubspec.lock index 1858b454..0c98c2c5 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -40,7 +40,7 @@ packages: path: ".." relative: true source: path - version: "0.0.0+2" + version: "0.0.1" flutter_test: dependency: "direct dev" description: flutter diff --git a/pubspec.yaml b/pubspec.yaml index bf7ad2d8..3bd24d10 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -3,7 +3,7 @@ description: A flutter implementation of React hooks. It adds a new kind of widg homepage: https://github.com/rrousselGit/flutter_hooks author: Remi Rousselet -version: 0.0.0+2 +version: 0.0.1 environment: sdk: ">=2.0.0-dev.68.0 <3.0.0" diff --git a/test/use_animation_controller_test.dart b/test/use_animation_controller_test.dart index d181f24c..96a1e676 100644 --- a/test/use_animation_controller_test.dart +++ b/test/use_animation_controller_test.dart @@ -22,10 +22,10 @@ void main() { expect(controller.animationBehavior, AnimationBehavior.normal); expect(controller.debugLabel, isNull); - controller.duration = const Duration(seconds: 1); - - // check has a ticker - controller.forward(); + controller + ..duration = const Duration(seconds: 1) + // check has a ticker + ..forward(); // dispose await tester.pumpWidget(const SizedBox()); From ed5c677df71ccdc0b6eeab65b14cc4cc57bcb409 Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Fri, 21 Dec 2018 14:41:40 +0100 Subject: [PATCH 023/384] useListenable --- lib/src/hook_impl.dart | 40 +++++++++++++++++ lib/src/hook_widget.dart | 37 ++++++++++++++++ test/use_animation_test.dart | 61 ++++++++++++++++++++++++++ test/use_listenable_test.dart | 62 ++++++++++++++++++++++++++ test/use_value_listenable_test.dart | 67 +++++++++++++++++++++++++++++ 5 files changed, 267 insertions(+) create mode 100644 test/use_animation_test.dart create mode 100644 test/use_listenable_test.dart create mode 100644 test/use_value_listenable_test.dart diff --git a/lib/src/hook_impl.dart b/lib/src/hook_impl.dart index 2d958efb..10de7006 100644 --- a/lib/src/hook_impl.dart +++ b/lib/src/hook_impl.dart @@ -219,6 +219,46 @@ class _AnimationControllerHookState } } +class _ListenableHook extends Hook { + final Listenable listenable; + + const _ListenableHook(this.listenable) : assert(listenable != null); + + @override + _ListenableStateHook createState() => _ListenableStateHook(); +} + +class _ListenableStateHook extends HookState { + @override + void initHook() { + super.initHook(); + hook.listenable.addListener(_listener); + } + + /// we do it manually instead of using [HookContext.useValueChanged] to win a split second. + @override + void didUpdateHook(_ListenableHook oldHook) { + super.didUpdateHook(oldHook); + if (hook.listenable != oldHook.listenable) { + oldHook.listenable.removeListener(_listener); + hook.listenable.addListener(_listener); + } + } + + @override + void build(HookContext context) {} + + void _listener() { + setState(() {}); + } + + @override + void dispose() { + super.dispose(); + hook.listenable.removeListener(_listener); + } +} + /// A [HookWidget] that defer its [HookWidget.build] to a callback class HookBuilder extends HookWidget { /// The callback used by [HookBuilder] to create a widget. diff --git a/lib/src/hook_widget.dart b/lib/src/hook_widget.dart index 02054a99..f8f132cb 100644 --- a/lib/src/hook_widget.dart +++ b/lib/src/hook_widget.dart @@ -366,6 +366,23 @@ This may happen if the call to `use` is made under some condition. )); } + @override + void useListenable(Listenable listenable) { + use(_ListenableHook(listenable)); + } + + @override + T useAnimation(Animation animation) { + useListenable(animation); + return animation.value; + } + + @override + T useValueListenable(ValueListenable valueListenable) { + useListenable(valueListenable); + return valueListenable.value; + } + @override TickerProvider useSingleTickerProvider() { return use(const _TickerProviderHook()); @@ -486,4 +503,24 @@ abstract class HookContext extends BuildContext { TickerProvider vsync, AnimationBehavior animationBehavior = AnimationBehavior.normal, }); + + /// Subscribes to [listenable] and mark the widget as needing build + /// whenever the listener is called. + void useListenable(Listenable listenable); + + /// Subscribes to [valueListenable] and return its value. + /// + /// See also: + /// * [ValueListenable] + /// * [HookContext.useListenable] + /// * [HookContext.useAnimation] + T useValueListenable(ValueListenable valueListenable); + + /// Subscribes to [animation] and return its value. + /// + /// See also: + /// * [Animation] + /// * [HookContext.useListenable] + /// * [HookContext.useValueListenable] + T useAnimation(Animation animation); } diff --git a/test/use_animation_test.dart b/test/use_animation_test.dart new file mode 100644 index 00000000..b8caf8b1 --- /dev/null +++ b/test/use_animation_test.dart @@ -0,0 +1,61 @@ +import 'package:flutter/widgets.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; + +import 'mock.dart'; + +void main() { + testWidgets('useAnimation throws with null', (tester) async { + await expectPump( + () => tester.pumpWidget(HookBuilder( + builder: (context) { + context.useAnimation(null); + return Container(); + }, + )), + throwsAssertionError); + }); + + testWidgets('useAnimation', (tester) async { + var listenable = AnimationController(vsync: tester); + double result; + + pump() => tester.pumpWidget(HookBuilder( + builder: (context) { + result = context.useAnimation(listenable); + return Container(); + }, + )); + + await pump(); + + final element = tester.firstElement(find.byType(HookBuilder)); + + expect(result, 0); + expect(element.dirty, false); + listenable.value++; + expect(element.dirty, true); + await tester.pump(); + expect(result, 1); + expect(element.dirty, false); + + final previousListenable = listenable; + listenable = AnimationController(vsync: tester); + + await pump(); + + expect(result, 0); + expect(element.dirty, false); + previousListenable.value++; + expect(element.dirty, false); + listenable.value++; + expect(element.dirty, true); + await tester.pump(); + expect(result, 1); + expect(element.dirty, false); + + await tester.pumpWidget(const SizedBox()); + + listenable.dispose(); + previousListenable.dispose(); + }); +} diff --git a/test/use_listenable_test.dart b/test/use_listenable_test.dart new file mode 100644 index 00000000..fdb579c2 --- /dev/null +++ b/test/use_listenable_test.dart @@ -0,0 +1,62 @@ +import 'package:flutter/widgets.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; + +import 'mock.dart'; + +void main() { + testWidgets('useListenable throws with null', (tester) async { + await expectPump( + () => tester.pumpWidget(HookBuilder( + builder: (context) { + context.useListenable(null); + return Container(); + }, + )), + throwsAssertionError); + }); + testWidgets('useListenable', (tester) async { + var listenable = ValueNotifier(0); + + pump() => tester.pumpWidget(HookBuilder( + builder: (context) { + context.useListenable(listenable); + return Container(); + }, + )); + + await pump(); + + final element = tester.firstElement(find.byType(HookBuilder)); + + // ignore: invalid_use_of_protected_member + expect(listenable.hasListeners, true); + expect(element.dirty, false); + listenable.value++; + expect(element.dirty, true); + await tester.pump(); + expect(element.dirty, false); + + final previousListenable = listenable; + listenable = ValueNotifier(0); + + await pump(); + + // ignore: invalid_use_of_protected_member + expect(previousListenable.hasListeners, false); + // ignore: invalid_use_of_protected_member + expect(listenable.hasListeners, true); + expect(element.dirty, false); + listenable.value++; + expect(element.dirty, true); + await tester.pump(); + expect(element.dirty, false); + + await tester.pumpWidget(const SizedBox()); + + // ignore: invalid_use_of_protected_member + expect(listenable.hasListeners, false); + + listenable.dispose(); + previousListenable.dispose(); + }); +} diff --git a/test/use_value_listenable_test.dart b/test/use_value_listenable_test.dart new file mode 100644 index 00000000..c721092d --- /dev/null +++ b/test/use_value_listenable_test.dart @@ -0,0 +1,67 @@ +import 'package:flutter/widgets.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; + +import 'mock.dart'; + +void main() { + testWidgets('useValueListenable throws with null', (tester) async { + await expectPump( + () => tester.pumpWidget(HookBuilder( + builder: (context) { + context.useValueListenable(null); + return Container(); + }, + )), + throwsAssertionError); + }); + testWidgets('useValueListenable', (tester) async { + var listenable = ValueNotifier(0); + int result; + + pump() => tester.pumpWidget(HookBuilder( + builder: (context) { + result = context.useValueListenable(listenable); + return Container(); + }, + )); + + await pump(); + + final element = tester.firstElement(find.byType(HookBuilder)); + + expect(result, 0); + // ignore: invalid_use_of_protected_member + expect(listenable.hasListeners, true); + expect(element.dirty, false); + listenable.value++; + expect(element.dirty, true); + await tester.pump(); + expect(result, 1); + expect(element.dirty, false); + + final previousListenable = listenable; + listenable = ValueNotifier(0); + + await pump(); + + expect(result, 0); + // ignore: invalid_use_of_protected_member + expect(previousListenable.hasListeners, false); + // ignore: invalid_use_of_protected_member + expect(listenable.hasListeners, true); + expect(element.dirty, false); + listenable.value++; + expect(element.dirty, true); + await tester.pump(); + expect(result, 1); + expect(element.dirty, false); + + await tester.pumpWidget(const SizedBox()); + + // ignore: invalid_use_of_protected_member + expect(listenable.hasListeners, false); + + listenable.dispose(); + previousListenable.dispose(); + }); +} From 583c5c56f169ba08b644bd91b67fb8da00492adf Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Fri, 21 Dec 2018 14:43:05 +0100 Subject: [PATCH 024/384] changelog --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 14032fab..fbd598d2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,9 @@ Added a few common hooks: - `useAnimationController` - `useSingleTickerProvider` +- `useListenable` +- `useValueListenable` +- `useAnimation` ## 0.0.0: From 6c2f3554586d29814775e1491f03a977c59493f8 Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Fri, 21 Dec 2018 15:58:34 +0100 Subject: [PATCH 025/384] useStream --- CHANGELOG.md | 1 + lib/src/hook.dart | 4 +- lib/src/hook_impl.dart | 92 +++++++++++++++++++++++++++++++ lib/src/hook_widget.dart | 31 ++++++++--- test/use_stream_test.dart | 112 ++++++++++++++++++++++++++++++++++++++ 5 files changed, 232 insertions(+), 8 deletions(-) create mode 100644 test/use_stream_test.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index fbd598d2..7d9cc300 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ Added a few common hooks: +- `useStream` - `useAnimationController` - `useSingleTickerProvider` - `useListenable` diff --git a/lib/src/hook.dart b/lib/src/hook.dart index fd0249b6..caecfc05 100644 --- a/lib/src/hook.dart +++ b/lib/src/hook.dart @@ -1,7 +1,9 @@ +import 'dart:async'; + import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; import 'package:flutter/widgets.dart'; -part 'hook_widget.dart'; part 'hook_impl.dart'; +part 'hook_widget.dart'; diff --git a/lib/src/hook_impl.dart b/lib/src/hook_impl.dart index 10de7006..293daad1 100644 --- a/lib/src/hook_impl.dart +++ b/lib/src/hook_impl.dart @@ -259,6 +259,98 @@ class _ListenableStateHook extends HookState { } } +class _StreamHook extends Hook> { + final Stream stream; + final T initialData; + + _StreamHook(this.stream, {this.initialData}); + + @override + _StreamHookState createState() => _StreamHookState(); +} + +/// a clone of [StreamBuilderBase] implementation +class _StreamHookState extends HookState, _StreamHook> { + StreamSubscription _subscription; + AsyncSnapshot _summary; + + @override + void initHook() { + super.initHook(); + _summary = initial(); + _subscribe(); + } + + @override + void didUpdateHook(_StreamHook oldWidget) { + super.didUpdateHook(oldWidget); + if (oldWidget.stream != hook.stream) { + if (_subscription != null) { + _unsubscribe(); + _summary = afterDisconnected(_summary); + } + _subscribe(); + } + } + + @override + void dispose() { + _unsubscribe(); + super.dispose(); + } + + void _subscribe() { + if (hook.stream != null) { + _subscription = hook.stream.listen((T data) { + setState(() { + _summary = afterData(_summary, data); + }); + }, onError: (Object error) { + setState(() { + _summary = afterError(_summary, error); + }); + }, onDone: () { + setState(() { + _summary = afterDone(_summary); + }); + }); + _summary = afterConnected(_summary); + } + } + + void _unsubscribe() { + if (_subscription != null) { + _subscription.cancel(); + _subscription = null; + } + } + + @override + AsyncSnapshot build(HookContext context) { + return _summary; + } + + AsyncSnapshot initial() => + AsyncSnapshot.withData(ConnectionState.none, hook.initialData); + + AsyncSnapshot afterConnected(AsyncSnapshot current) => + current.inState(ConnectionState.waiting); + + AsyncSnapshot afterData(AsyncSnapshot current, T data) { + return AsyncSnapshot.withData(ConnectionState.active, data); + } + + AsyncSnapshot afterError(AsyncSnapshot current, Object error) { + return AsyncSnapshot.withError(ConnectionState.active, error); + } + + AsyncSnapshot afterDone(AsyncSnapshot current) => + current.inState(ConnectionState.done); + + AsyncSnapshot afterDisconnected(AsyncSnapshot current) => + current.inState(ConnectionState.none); +} + /// A [HookWidget] that defer its [HookWidget.build] to a callback class HookBuilder extends HookWidget { /// The callback used by [HookBuilder] to create a widget. diff --git a/lib/src/hook_widget.dart b/lib/src/hook_widget.dart index f8f132cb..df7e859a 100644 --- a/lib/src/hook_widget.dart +++ b/lib/src/hook_widget.dart @@ -383,6 +383,11 @@ This may happen if the call to `use` is made under some condition. return valueListenable.value; } + @override + AsyncSnapshot useStream(Stream stream, {T initialData}) { + return use(_StreamHook(stream, initialData: initialData)); + } + @override TickerProvider useSingleTickerProvider() { return use(const _TickerProviderHook()); @@ -494,6 +499,7 @@ abstract class HookContext extends BuildContext { /// /// See also: /// * [AnimationController] + /// * [HookContext.useAnimation] AnimationController useAnimationController({ Duration duration, String debugLabel, @@ -504,23 +510,34 @@ abstract class HookContext extends BuildContext { AnimationBehavior animationBehavior = AnimationBehavior.normal, }); - /// Subscribes to [listenable] and mark the widget as needing build + /// Subscribes to a [Listenable] and mark the widget as needing build /// whenever the listener is called. + /// + /// See also: + /// * [Listenable] + /// * [HookContext.useValueListenable], [HookContext.useAnimation], [HookContext.useStream] void useListenable(Listenable listenable); - /// Subscribes to [valueListenable] and return its value. + /// Subscribes to a [ValueListenable] and return its value. /// /// See also: /// * [ValueListenable] - /// * [HookContext.useListenable] - /// * [HookContext.useAnimation] + /// * [HookContext.useListenable], [HookContext.useAnimation], [HookContext.useStream] T useValueListenable(ValueListenable valueListenable); - /// Subscribes to [animation] and return its value. + /// Subscribes to an [Animation] and return its value. /// /// See also: /// * [Animation] - /// * [HookContext.useListenable] - /// * [HookContext.useValueListenable] + /// * [HookContext.useValueListenable], [HookContext.useListenable], [HookContext.useStream] T useAnimation(Animation animation); + + /// Subscribes to a [Stream] and return its current state in an [AsyncSnapshot]. + /// + /// [initialData] is used + /// + /// See also: + /// * [Stream] + /// * [HookContext.useValueListenable], [HookContext.useListenable], [HookContext.useAnimation] + AsyncSnapshot useStream(Stream stream, {T initialData}); } diff --git a/test/use_stream_test.dart b/test/use_stream_test.dart new file mode 100644 index 00000000..5e6ea01f --- /dev/null +++ b/test/use_stream_test.dart @@ -0,0 +1,112 @@ +import 'dart:async'; + +import 'package:flutter/widgets.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; + +import 'mock.dart'; + +/// port of [StreamBuilder] +/// +void main() { + Widget Function(HookContext) snapshotText(Stream stream, + {String initialData}) { + return (context) { + final snapshot = context.useStream(stream, initialData: initialData); + return Text(snapshot.toString(), textDirection: TextDirection.ltr); + }; + } + + testWidgets('gracefully handles transition from null stream', + (WidgetTester tester) async { + await tester.pumpWidget(HookBuilder(builder: snapshotText(null))); + expect(find.text('AsyncSnapshot(ConnectionState.none, null, null)'), + findsOneWidget); + final controller = StreamController(); + await tester + .pumpWidget(HookBuilder(builder: snapshotText(controller.stream))); + expect( + find.text('AsyncSnapshot(ConnectionState.waiting, null, null)'), + findsOneWidget); + }); + testWidgets('gracefully handles transition to null stream', + (WidgetTester tester) async { + final controller = StreamController(); + await tester + .pumpWidget(HookBuilder(builder: snapshotText(controller.stream))); + expect( + find.text('AsyncSnapshot(ConnectionState.waiting, null, null)'), + findsOneWidget); + await tester.pumpWidget(HookBuilder(builder: snapshotText(null))); + expect(find.text('AsyncSnapshot(ConnectionState.none, null, null)'), + findsOneWidget); + }); + testWidgets('gracefully handles transition to other stream', + (WidgetTester tester) async { + final controllerA = StreamController(); + final controllerB = StreamController(); + await tester + .pumpWidget(HookBuilder(builder: snapshotText(controllerA.stream))); + expect( + find.text('AsyncSnapshot(ConnectionState.waiting, null, null)'), + findsOneWidget); + await tester + .pumpWidget(HookBuilder(builder: snapshotText(controllerB.stream))); + controllerB.add('B'); + controllerA.add('A'); + await eventFiring(tester); + expect(find.text('AsyncSnapshot(ConnectionState.active, B, null)'), + findsOneWidget); + }); + testWidgets('tracks events and errors of stream until completion', + (WidgetTester tester) async { + final controller = StreamController(); + await tester + .pumpWidget(HookBuilder(builder: snapshotText(controller.stream))); + expect( + find.text('AsyncSnapshot(ConnectionState.waiting, null, null)'), + findsOneWidget); + controller..add('1')..add('2'); + await eventFiring(tester); + expect(find.text('AsyncSnapshot(ConnectionState.active, 2, null)'), + findsOneWidget); + controller + ..add('3') + ..addError('bad'); + await eventFiring(tester); + expect( + find.text('AsyncSnapshot(ConnectionState.active, null, bad)'), + findsOneWidget); + controller.add('4'); + await controller.close(); + await eventFiring(tester); + expect(find.text('AsyncSnapshot(ConnectionState.done, 4, null)'), + findsOneWidget); + }); + testWidgets('runs the builder using given initial data', + (WidgetTester tester) async { + final controller = StreamController(); + await tester.pumpWidget(HookBuilder( + builder: snapshotText(controller.stream, initialData: 'I'), + )); + expect(find.text('AsyncSnapshot(ConnectionState.waiting, I, null)'), + findsOneWidget); + }); + testWidgets('ignores initialData when reconfiguring', + (WidgetTester tester) async { + await tester.pumpWidget(HookBuilder( + builder: snapshotText(null, initialData: 'I'), + )); + expect(find.text('AsyncSnapshot(ConnectionState.none, I, null)'), + findsOneWidget); + final controller = StreamController(); + await tester.pumpWidget(HookBuilder( + builder: snapshotText(controller.stream, initialData: 'Ignored'), + )); + expect(find.text('AsyncSnapshot(ConnectionState.waiting, I, null)'), + findsOneWidget); + }); +} + +Future eventFiring(WidgetTester tester) async { + await tester.pump(Duration.zero); +} From 7d763ec3fbcd7fcd8a4b6f49ba1e0634510c1ca8 Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Fri, 21 Dec 2018 16:31:06 +0100 Subject: [PATCH 026/384] useFuture --- CHANGELOG.md | 1 + lib/src/hook_impl.dart | 74 ++++++++++++++++++++++ lib/src/hook_widget.dart | 14 ++++- test/use_future_test.dart | 127 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 214 insertions(+), 2 deletions(-) create mode 100644 test/use_future_test.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index 7d9cc300..4eec888c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ Added a few common hooks: - `useStream` +- `useFuture` - `useAnimationController` - `useSingleTickerProvider` - `useListenable` diff --git a/lib/src/hook_impl.dart b/lib/src/hook_impl.dart index 293daad1..9a9bf2ed 100644 --- a/lib/src/hook_impl.dart +++ b/lib/src/hook_impl.dart @@ -259,6 +259,80 @@ class _ListenableStateHook extends HookState { } } +class _FutureHook extends Hook> { + final Future future; + final T initialData; + + const _FutureHook(this.future, {this.initialData}); + + @override + _FutureStateHook createState() => _FutureStateHook(); +} + +class _FutureStateHook extends HookState, _FutureHook> { + /// 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; + AsyncSnapshot _snapshot; + + @override + void initHook() { + super.initHook(); + _snapshot = + AsyncSnapshot.withData(ConnectionState.none, hook.initialData); + _subscribe(); + } + + @override + void didUpdateHook(_FutureHook oldHook) { + super.didUpdateHook(oldHook); + if (oldHook.future != hook.future) { + if (_activeCallbackIdentity != null) { + _unsubscribe(); + _snapshot = _snapshot.inState(ConnectionState.none); + } + _subscribe(); + } + } + + @override + void dispose() { + _unsubscribe(); + super.dispose(); + } + + void _subscribe() { + if (hook.future != null) { + final callbackIdentity = Object(); + _activeCallbackIdentity = callbackIdentity; + hook.future.then((T data) { + if (_activeCallbackIdentity == callbackIdentity) { + setState(() { + _snapshot = AsyncSnapshot.withData(ConnectionState.done, data); + }); + } + }, onError: (Object error) { + if (_activeCallbackIdentity == callbackIdentity) { + setState(() { + _snapshot = AsyncSnapshot.withError(ConnectionState.done, error); + }); + } + }); + _snapshot = _snapshot.inState(ConnectionState.waiting); + } + } + + void _unsubscribe() { + _activeCallbackIdentity = null; + } + + @override + AsyncSnapshot build(HookContext context) { + return _snapshot; + } +} + class _StreamHook extends Hook> { final Stream stream; final T initialData; diff --git a/lib/src/hook_widget.dart b/lib/src/hook_widget.dart index df7e859a..db097878 100644 --- a/lib/src/hook_widget.dart +++ b/lib/src/hook_widget.dart @@ -388,6 +388,11 @@ This may happen if the call to `use` is made under some condition. return use(_StreamHook(stream, initialData: initialData)); } + @override + AsyncSnapshot useFuture(Future future, {T initialData}) { + return use(_FutureHook(future, initialData: initialData)); + } + @override TickerProvider useSingleTickerProvider() { return use(const _TickerProviderHook()); @@ -534,10 +539,15 @@ abstract class HookContext extends BuildContext { /// Subscribes to a [Stream] and return its current state in an [AsyncSnapshot]. /// - /// [initialData] is used - /// /// See also: /// * [Stream] /// * [HookContext.useValueListenable], [HookContext.useListenable], [HookContext.useAnimation] AsyncSnapshot useStream(Stream stream, {T initialData}); + + /// Subscribes to a [Future] and return its current state in an [AsyncSnapshot]. + /// + /// See also: + /// * [Future] + /// * [HookContext.useValueListenable], [HookContext.useListenable], [HookContext.useAnimation] + AsyncSnapshot useFuture(Future future, {T initialData}); } diff --git a/test/use_future_test.dart b/test/use_future_test.dart new file mode 100644 index 00000000..f5692a3c --- /dev/null +++ b/test/use_future_test.dart @@ -0,0 +1,127 @@ +import 'dart:async'; + +import 'package:flutter/widgets.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; + +import 'mock.dart'; + +void main() { + Widget Function(HookContext) snapshotText(Future stream, + {String initialData}) { + return (context) { + final snapshot = context.useFuture(stream, initialData: initialData); + return Text(snapshot.toString(), textDirection: TextDirection.ltr); + }; + } + + testWidgets('gracefully handles transition from null future', + (WidgetTester tester) async { + await tester.pumpWidget(HookBuilder(builder: snapshotText(null))); + expect(find.text('AsyncSnapshot(ConnectionState.none, null, null)'), + findsOneWidget); + final completer = Completer(); + await tester + .pumpWidget(HookBuilder(builder: snapshotText(completer.future))); + expect( + find.text('AsyncSnapshot(ConnectionState.waiting, null, null)'), + findsOneWidget); + }); + testWidgets('gracefully handles transition to null future', + (WidgetTester tester) async { + final completer = Completer(); + await tester + .pumpWidget(HookBuilder(builder: snapshotText(completer.future))); + expect( + find.text('AsyncSnapshot(ConnectionState.waiting, null, null)'), + findsOneWidget); + await tester.pumpWidget(HookBuilder(builder: snapshotText(null))); + expect(find.text('AsyncSnapshot(ConnectionState.none, null, null)'), + findsOneWidget); + completer.complete('hello'); + await eventFiring(tester); + expect(find.text('AsyncSnapshot(ConnectionState.none, null, null)'), + findsOneWidget); + }); + testWidgets('gracefully handles transition to other future', + (WidgetTester tester) async { + final completerA = Completer(); + final completerB = Completer(); + await tester + .pumpWidget(HookBuilder(builder: snapshotText(completerA.future))); + expect( + find.text('AsyncSnapshot(ConnectionState.waiting, null, null)'), + findsOneWidget); + await tester + .pumpWidget(HookBuilder(builder: snapshotText(completerB.future))); + expect( + find.text('AsyncSnapshot(ConnectionState.waiting, null, null)'), + findsOneWidget); + completerB.complete('B'); + completerA.complete('A'); + await eventFiring(tester); + expect(find.text('AsyncSnapshot(ConnectionState.done, B, null)'), + findsOneWidget); + }); + testWidgets('tracks life-cycle of Future to success', + (WidgetTester tester) async { + final completer = Completer(); + await tester + .pumpWidget(HookBuilder(builder: snapshotText(completer.future))); + expect( + find.text('AsyncSnapshot(ConnectionState.waiting, null, null)'), + findsOneWidget); + completer.complete('hello'); + await eventFiring(tester); + expect( + find.text('AsyncSnapshot(ConnectionState.done, hello, null)'), + findsOneWidget); + }); + testWidgets('tracks life-cycle of Future to error', + (WidgetTester tester) async { + final completer = Completer(); + await tester + .pumpWidget(HookBuilder(builder: snapshotText(completer.future))); + expect( + find.text('AsyncSnapshot(ConnectionState.waiting, null, null)'), + findsOneWidget); + completer.completeError('bad'); + await eventFiring(tester); + expect(find.text('AsyncSnapshot(ConnectionState.done, null, bad)'), + findsOneWidget); + }); + testWidgets('runs the builder using given initial data', + (WidgetTester tester) async { + await tester.pumpWidget(HookBuilder( + builder: snapshotText( + null, + initialData: 'I', + ), + )); + expect(find.text('AsyncSnapshot(ConnectionState.none, I, null)'), + findsOneWidget); + }); + testWidgets('ignores initialData when reconfiguring', + (WidgetTester tester) async { + await tester.pumpWidget(HookBuilder( + builder: snapshotText( + null, + initialData: 'I', + ), + )); + expect(find.text('AsyncSnapshot(ConnectionState.none, I, null)'), + findsOneWidget); + final completer = Completer(); + await tester.pumpWidget(HookBuilder( + builder: snapshotText( + completer.future, + initialData: 'Ignored', + ), + )); + expect(find.text('AsyncSnapshot(ConnectionState.waiting, I, null)'), + findsOneWidget); + }); +} + +Future eventFiring(WidgetTester tester) async { + await tester.pump(Duration.zero); +} From cb3401002f9226c668e8e402d33489670b32f87e Mon Sep 17 00:00:00 2001 From: Robert Felker Date: Sun, 23 Dec 2018 22:25:41 +0100 Subject: [PATCH 027/384] Logo Proposition (#7) --- README.md | 2 +- flutter-hook.svg | 1 + hooks.png | Bin 13086 -> 0 bytes 3 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 flutter-hook.svg delete mode 100644 hooks.png diff --git a/README.md b/README.md index a42cf433..7c339b65 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ [![pub package](https://img.shields.io/pub/v/flutter_hooks.svg)](https://pub.dartlang.org/packages/flutter_hooks) -![alt text](https://raw.githubusercontent.com/rrousselGit/flutter_hooks/master/hooks.png) + # Flutter Hooks diff --git a/flutter-hook.svg b/flutter-hook.svg new file mode 100644 index 00000000..260b868e --- /dev/null +++ b/flutter-hook.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/hooks.png b/hooks.png deleted file mode 100644 index 198120364c14e9b9c8edb5df32bbfa16e98ad9c7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 13086 zcmb_jwL*|_S~~GbI)#~G(ITcVN+luAtB)@Daw6Bj8p$zn6D6b@1nEMhymI2qk;@l z%>>l};sMQ8T1^@WsUCv!WR8w_#&T2C_e4S>F#qpDE|V;^M?&IjRFad{_Axoh#_}hb zzlNF)7JD5%x3)a5I7|mspY$x>xEel|*2r5ct*0QdZHFjpx_$ayhZzRO3r+}=LXiQ` zIRyh`Xh%n}qv=npgBD28>3ko@Ua~ShBg8LHySk9rif$k8vnD%lS3PZ+@3S_5{Po_J|I!pNz36ex>CHq*^ z#Y;=R*LLqcXXnqMd^rpLvUdF%bHAgqrsEKoDyUsa#FU4Lai|=XnKKYQIl6B`;e70u zGwnpMLnz7qZ6S+QfX`#Ew5!*_%)j^hF)9klk-w@nvYs%b91Yd1K!akF76oN)&p&pU z)$i?kdxKp=f@;FMRi%vg=gVXShKEOrHM0CQwp$Fe|+%;8s3yDG>cLe6ugqUH@DErp5yQWeUB7i$R z&c*dnK&HH5uI8<@iH7-ihk&FHDT5!8>3D1^@E7-10KN(}`;^Do89Tm5 z_Sk=KW|TK$8M%kSzfNrRJ|BDNTvlla7|&hn)3DSXGoUAdoWFHvzn2^MOmmSU5%{+x z({&vktdDMSSvE>b6CDA@mvaFwh*DkcP`Oi|8N#uLcrBH9Qm*C%nez8ZkpsIffy7co z>%Q>0Ux}ffsWsR)UcM%0GsUCV03rrjhm1Vuyhv4Okn=@)d~QZbOQgso*ZX`CW-Bb~ zRJSRcZ7BxQOU>E2R~-`Ryf@R+{1^Q>;-|}Bo^QMV0Ko`M>usy`k^uo;LVu6E`7J>lYgSh(w2)aS!j&O?tS9k$0G$;Ygw1<2=x;(1>OJf213#B3=KG!f6i)6ZYf=*@B!ApZIBVmwR2hS5mB}QM zNd%WU%A{zMsn}#XfpX7Y0j7e<`EAhN$C4$!a}U^BUGm&L&Jh3N;zxhfHDDby5iE!S3p`^XwUKXs2cIpBv~C8hlOwIiNf6SvV3p z_(YrmfbuEio0)BEpbU{DG^^P#{@fI=MyS9>h-co%GMRi{%VDe3A3w5_lapti`@^vO zsK4&#?`Mg6zM2)7mEdW4=Z~ID4aC?fx9{CcP=c1*qgYy>W6O7HTnsX1Z>qD_(@y9m zRqNGns{$r3D|M@*aVdqW+~&$O_e=xf*OERrOSXSxB~hN$0MH83TvX=hdSm2~yc1Z@ zkiaLh<>aX2gfFix#TR(Px5H>v*sidYNH1MJr~x-^ zQTl~A){Mf#!;t}M#3?Q$ZXdp5*Ph|gfFv4R);h8!@b@+=NvJURlh!SGp@V(y2zC)I z5w90rY<(b(4y#w8R_0)c8nMmn!i?pWK3?m*{Zyy`dA8f?51;lsZD)D7npLNv5_U1d zOmUkY&lIlRbAKWsT){lW7!wh4N}jwS8}v!^@f;~dD-sbpPQrk1lU@4~4XcTeo$*-= zM!p+O;fxW>{&13F=rW!2^{{-O0U}Y*cX1+326Y%p^>5LM%y>l%c|4oilj@h4ZOb;z zkHlfZSEsf|8S#%4ff((_5Xx$5PM`i@|McFa>wI{BaWV)G10qiXzO6<{w_X{C9hM+is&9T#qe}AnJQo?P9xHlb;j2y!{dfYio0Y zZ*by9Qp{Jy7{6Jj7i#eExU&YUDha-+j1|bYn^eF%0r@m`Q8BAiDa@OoIUurud6K)xG_u8OlHY>o9~vzvGq4ZufBkB zJe_|}T_e0nI`ozO%NECFoBZETxDpEdky#_)H@1HH@7%$!J;?Hk2Ru*61vJ*qY z(-a$uWN4U-RA)xE31NjnY34n7d7IpZtuFJg@Tnq60Zpr`M$UJzt;u?`5U2rikMoSD zq?a1hGEUw{f~+%E%_V$$&@B1$%omqF8h#Aq9=gbaf`Xocpoithc>V0 z$B!TOZ{NQ4LH~dN$Q~#YvBX|9F^6GHxyB^XOBi=f;X#O1&&-oDxpj$_TcI2s-v^9h z0kqiv>YZR2t2YtlPjQ8PHSqSn!8=;0j#>8@<($61+;g;q#)BpK`7ep6BY3T5Y-u*h z1o2pN&Nf6`rYBLDV0wfgNC&n&G_UAr-`Ytv;CT#%>lGI!R~}=TknoQ6ai(jxXouZ& z0V~JO;h!pfPvjL$DNnbT<3?H!{yNudce^nFfehHC<+98^2-N9=Pyp6o3nIv4`3e_c z6y3_n@?^Q$@?4i10tIq%aKwTh_6v6oQakZzb8r&JnfM2#%(3VuQ2C=!9(8G;6SW$P z^vrnbTro;OUTT12q}@<7iLsE=BJ8kAzp-H88{X{20+S>YHwp4vX-1_(T7wcdY z_ZI8pTqE375QUdzp6Ia15akLHlsFm~!cjiO!kC@t1N)t7I5W8CwAAnqrA&loYrfU% zXyG6|J>5@AwI|OUwew^Ov>3o?BSy{6*JEv>DxW;A$x8HImDVwn^18mCk&#y9rTq=SAy7Z`SWx;rO8aBVn?S_X0Wzf2+D3#(alwCEjDfKw$v5U&>s`NW1+3CZv` z5{ht+F+$XG6Dw>)jte!N00B_qKD~>03c*7G|42;jTw_o!w}kic;;$WuB(-t91v*Gk zj#=b}{j*5wD(-O%$dPhP@$35D`_r(z_L&~7d^=L*ytKicfI|yYBZnbv+-L+j|D(0HD(Ztg~s6L*Jeou1_nO!{;*$9_m`u;M@M^P^OPC?3HN|b z-^JIDy4!JYiBITNXkq7avF+op{AWC0qYY2^9F~_hv^Xshy%BLU6JTLsK>RaSPk|sVUL7eMdt(O|Cd(s3Qb>9W}x*+6r`swKj zj94*`m1`0(rDTdH-Y7`zDl~)_7FxqD5U1_!cy zh~4G~yynoWb3T|UmL%RCc7k-2?Bwa`UvJ4IL}OZ(*~?_H44eID$FXEbfXUiaun{@> z=wD(i_N#5~vXzyUs0c3;MNCRcgG{JKF6c1VT2Dz_O!2nY$tu&rReLy&Z<^ZmuYEhx1g$471L&t?x-2e-Yx0F#Q8sKm2Q z1{A*C$eobM7QU!+%zM=pO;)k61i6JUxt@bib%%^)`nyKv%iZxu%nE>Ex3#TpkD%|x z-;3Ho>_NymX?LDP(^)8;_2TUhTOz@skE>XZSz;+DC09Np!-uNRMzf)I>C9h$;q8 z0st)|unb&^&D6}pG9@0XZ9#$$~0#;)S-H|`YH$Rvtbd>l!_^yeMriUr2T zsi*V(BVb};^7HWYTxzV9mdFi_jO`?Vk;Fz}#7_QT7QjH@f_Cf(cDZIpEgXRBp{bH35HAuf86*W4ND_)s)N1ey4hFXf`o^)zasfp?)Ryl3{fleZ?qLm(BQ|bx z%YSt<(qKL6ViSzELG7isDdZLl(aMbJ)0LYO0PHI6<>zK#4@C_zZ|QwpiRDGM&+y)A zbKkk?_25iOObqVn4pOJBUhM`yhLgz-b5nno!~GWd%bI?orHH;?=mFvNv-Ae7c7i6$ zWENlhujYPFPZy=<=AP{FzxQCyd#hDq$K8FmZP$W~vaKO8Qvo2z0P+(&dao4lCTp}o zs$`*g8cjIrJck)bl0aN9-&`i1$bZ}B1K*ri8f^(W&+sH)-m02tz%>+ z4Rwv_V7^Nm{Li6emufV{Bytk(qdeQae)e%=$7H2so6-l+ zXpOsx&sjx+?J_Hn;uE=T#HrGAchg#!Py)oIy%pV}Japm%-seV3HH?y;cQ`P>KUTtp zXA%k;k(C%#evu)YQ|7w9Lr4jjyXUFl2H|HNxw~K}Ad%;(nPL@V5&Uma+}L3_{anG8 zMAZHz(RT8?q`QO1IFI|KTt`~7b0EU2MfiF9#Ijp-rBK?5eY{Y|QNKc7b-RDxzy!rh z)%U{%M^^1={UF8$HEowgXlD&HmYm;ewnUASgoI?dNGaLvzGOUC^5q{Y27Xq6Kd zD9zcr4s|;{XzJHc!oA1>uN~#%dxqOjGLyekVzRn7^*$NH)XwnXd6hx{ulWbTPk07F zCi;*3wrZOsWZZe>(OQJcTwH(yUz~W&WK9hXShf9Z2?@fy3-fvIs&*Yo4M26P4VoQ< z!Uv@u6PbDAHThqW4ea!{A1{W1#9dy0M*@uAa=!Z`ru#L;!M1pz)hJAbzl@9hUECIf zy?6lyrYs<4X|)+SySbiz{*|;@u=?Xd%lc^*j*)VvPL{G1AMbg>5M%LtD*`*zb=r>L zz3eYdEIB?h5iLTt6IlyMNlA`x<*=O;hzV89`fM4SinE(+#xpj0F90-HV|k)b2Kw1& zfMd_Que#uS23sAK=vH2_X=9pzkrNz;oNoY3MqQC_X2%cRdHD3$p=ltsY8Ud;3eXe# za@@8yGErm%WKOWM)=QW{bB{Y=BLyKB{WCwG61u=-X=9u!<` zfHq`FBqttH&mTFP)%=W!c_3)p+!N+qE|Ol)2vk>PCOhP(!9mJfLzuL;mgmCi`G z<|YM1-X2<357G{ zJk&`n^;vBz>T#GgEVelJU(_%Ikfh=4#mrN}%?(VuTTju+XsS{U6nT>w2?+nr8GcFo zv{3#{lEMgqO02GDQoKvisti4jl76?UfM{W?{c+)qr-SXGm?LJj3~ki;edsE;%w?NC z`tZ}7VBZrU*MRZ?mGMYKUCRn5axbV8gto_eLyFcmf@Wpgu;}CYmb#J%yPV32;!Vr8 zRey#X=7Z1nxw{C)aufK``{tftJsAFUIqACi)9<$3DFlz_kG44x@-43;09M;!-^4kLYo60vn>>Y;-k=qY=@H5r!ovh?oj zADB7igshMSdcUDh3YRfOz};3vgaJaiQJ%Vz$Voz$TMAHm>WH<
1S1Laos6QJ|z7{|e6XW&EQ2+{gsjBUfcg=7v> zlWR*8D&5u_{N{Z554ol~#jYr-JUlzuCWD8e6^-=`4Sdm`Hlw4X`(KdkKpXF6RM^A( zTYg~q^Fk3}PL9IG#YG4r6MOXZ^sJ>9M%}6Q{Hh&rZmihyIGZ8!5B}x-t*=9`}vZl=Q{n#uTA@S?$3WP(RL@1 zN5!4sQ3`SN(M)9Un1{4jjU-L|yj>R$8`k3>Tfxtq!Y77oCxQx;Te1m2cbmfq#}fE3 z#y=TG%5BmWyE^==pmSJy>##8S?Vp;kt+QKBC)a9H3-AmdlW#gQYA<{ch(3r1ya;1y zn}XOjfWHi`;%zA|e>kaYCPvYKjfUa3Qorwa~6Shd`G zr(Th0NV&{@sf}u8W-)iQ>RI>W>7@7R>ir>N*%l%aE>=)ijxqA_Wrw#(py@vM!Fc}G zL0n?>>4QcP*d}z8N|~!NsIBuGkli6zjMc*a!e!w3;Qvoas;OIr6DXl>s z_GBQnyggkr-uN#BJZC4qyW8}mp-DWb!pHTTS6C<#lwZGIK$kVkFjVE^BTnFut|y9u z`z)A*wAw~Z{5VlK-jOHZ?v}m_@mq`AI=B+v-{4MQ{e`@~1A8%iPl7ra-_6;C+P4~Nr z5~&21*U@2E;i^|lSgbiCNsBJsUSnJzY!>v6TH1Um3JsPg+|bw@jU8lZo7LmIb1H;3 z00`z~YEuH-sjO%O>uz!LY=(zz4MgmWJfRD43^fpF$@lR(3`?Dh2p@d2fs2>V_icb4 zJ{~1syFOJm*Oe&-!tx`od{s=RTvwi+r_qubsj*q`COoZF1nX%giPYT0lgCLPTfV7i znDQLDMEc|J33qN9xqNW=d1LrM`u4*sR?Ty{ZewI~Cu{3}gC!*;Q%&}>O?0x~HZdxW zD+q>vw*=?W?9H$XmzRl<&6YAJ99Nx7ZaT-mZa?gQ#1KSp_KNGg)~VDz_<~PSM9N`Y zB3buPoHhsqW__YTY~}Fu3PE-(yCzT4fYhq1t^2^o0#l)Ht@!Onp>#gr`IVya&kJa? zdCO!FW!)^f$Pra8R9HG%n6|dIk2(35qOvkYzy17(rcMM3MPmz37O@fe+h#I$*-=&N zg!0m9&xjVK0T#>fnGZ$3f)VJ+!$691Zu3vdk3WY;e`^1@cAf~mt2IjmeIC)RHU+0K z(V4dB1A+4WwQcTwkswE+(WK>xc$S4#0=sn^Z1Yc^u1qJ{v7>*e@zG)e#uLhy(do_Li0tPPu=X9pVDJ_ zV~H0rF{+{^$z!6n~mKn=)n9FSU)Ou2#>I(4U+zE-QVvvG`iyyukd2 zlH^3O0X=9yYX45hwW?}8JMmAp;4^8KPB8I9E2|hKV-7w}8Dht158)}-<4qmY5W66M zw<2f)_G&s$9}qd6BMO(#-nfVGdy^0*71nAxs|tKo39AOiLA2&|x*(KZ&peDu(9A`J z$-M34_yRwRngxmn$$axTa2qn=xUz80F+bRCw@s!?L69{dZ4F;sBQ`#~2tPgRJb{r^ zEPwV;1gOgWwUFA-tI_Y{^66YmGa- zNp$n8syI$=R$K|z+@WvO*H6S@q^_QfPHI8|yoA0MhU|pupY><(%fyPTYZj~2Lk>gf zWsu*&EE=_Ow~e@{T>N`hUDqH^8_uRSp`Uy#VFKUa>u;tB`g&fc*#8mp$e8C8d@27G zCWF%doA@@H_zYr*NGmaLNV%+pebzlE-#re;QGRKxqXVD}c_LH@~oAC<{tUz{s(74Jq-R{ABX^r*+-fGWMQ;TqzH&8HH&uE|D*j(&Yg zvfb4Z>c}!AC$zyzKsO)ZxzK|?0C`V_pvI`3FK7c8@*D6*z-}tO^G}!9xa_CpG$~r? zY9%&95HDR~6n%gDL38zTLchmPvuS#GJ(T9ppDM%B$zK!GuBI>X_LRBrriL0+Lu{D%r1%jr5o2U4h}a@rBV4x|){i6o zR5O9tAuQC|8v~!@pU_Y%4t#M|vS_d(SuQwt2&r9-@We>zsf|k)iZC9gerFSFqT4N> z`{PsDM)P$cnLC*?kGr^T@|#a9&Vm#7dAA#-;Q}$ zy%&AJp=7^GtOSdh=p{H~s2Np^49(a$t2CMYTA>0@>QF8oR$)nmOhy(;s^{;MF%m7c zs#sP4?vorG9G)K#PQItn=X`sLe^TOdscDG2>%nXs+85xqeOA$+?%~pa1`RN%HtlhQ zXD(arlpb_vcV5-c+8kXjI87_KF6M94pER~z`ZDd%6G378lK8p%Oymq6afX0~muJD&w6nSkXkLws8T z%iAjr)s#4xyHt_V?pmGUa+GO5m2lqm0S`~9m*$T<Ae?qs$ngz;{C`eKga#j{PW zv%_bjb;uHNK18TZqs);gxp{4B3RbzgtrMtPiPv@1NAp9bPfxBlyie7xv6#C1BZq4Y zoUAWlOIBFIXCs@tZDXD+Yp(SWcI`59>$FMsBZ!zq_^#3$)nDPg52dW8)BuxEOr9(w z3bFb_OeB;oM*|wpGUlI*_XS-GLDM?3Go(GXs2GHefkcoBnP@HYC-|EeKr1)#=6q=O zogtg5#DM5J^Ai)g|D0AHeKW|-vDOA2(WY9Mtw@NDW+SEme8e77Qe)$&?h-F>^+1lY zF_JF)J;;vtn9<5j7VL{5p*FpCHx;)Is%Cedk(1#4HQZZTk+eORZ+l>9+kV8revX~a zN-c22WGDQ5rRYo>ZTjm{=$mb^ZtGW<3;T^ywV@!Ct@kzFnkXN$Jc<@x%`ROcqt%mr zBUtOD>2$yhJg6Zcxh==Ka$i8_QoCxmW2BwstA0l-v^x=M^279*r&(yzTlbNhE>*_e zBM8#qv2bU&G!u&h#pqwkNAJA_fjh}S){L!H?h@ULiH7(QfFRZ(yprmWUh@!XQ zx+~g~B;H{9S@Q3;#^J@|6BvdR2|c?;FF1Ku4*d#wgZ#ACKrxP=4m^`oWK zPk$eVw}O&(r^&O92C74L2=tq7M6Vd)T^j638#;pv4jqQVwhJ=xn z@mBV;EoC!K=a2gcMpx`EKl$Pk_*YMjPJh{$1-#?KaZZYWM1mf_FlOd=!j*MtWQ-_; zkLS3S@6<9*opD=b0=<;Ijs#sK)RQ7j5S$aN?CE=un#xk^di|N5fb&?F;;zllSMjKY z*9LY#YPbh?ZRF{GOTFNfvzFA6?y>{sFk7Bg-BD{pZ6YOo5>r>UnHZ6yAj9jFggf7) zQjq`D8rgZ@#c4T6Fd|7u{-ZyqzwI^Y#?R02MB8NHS2tc8udlzb zw-caWNoE>ceU_HmP8wfM#d?1X<(K6Vf(wOS*1s|MH49kIHCW@rl7tjUYUPVaS1p2f zi;v)`L1qt(spu(RR{pvkbTe602&~H0=eZL`b&vLbb&jyk`sX3YwFV<@5X%`pJKGxp*J=oux+y7m*9`Zs~2GJFMav9^ia+ zA7hKX8KjRPedPT{Qp-z(^f-5dQ+;Ge`b_!V)QeZz{ps`C)N+rZK8%($m=#GpIZA$4 z_R*5uWJZYPvjW;|qTcl48^Cp6BxN8`v8X{30^B~e%TmH#Bp=*m?uUL)2Cn}!TCrC1 zpC%%SaXQ8>llj^5xT5Cc!q*oGzlnu2Vn;%aY3$vE!>A7oDIYvHl;NFMdu+rkIe+o0 zeb;{XGZVtR{cPfzu`#)sjcoOY&totf9A-58+y>4>eKQ%V>x>Ko9Iq2Mw$&dh9SZLZ z?*1(=NP2Bh*z8Fc=nwUR5%V?Yz}~M=z@vC&aZ+NO`K`5M;qZ9_H0=?3OQzw zKZtXX-n#q7w9{X_pyIfU^mPx%941b=Y>Hnsk%;;t!R1@ViwFnAkTd*C*VcGHeQp`% z1y-I5JtW!SF;*cU*f$CIOl~Wv&!ntvbeDgaJ9<#BZ-s^R4Og;NDU>N=mQ(_lJ_E8R zSl+xGm=8OEkwm+u;QVg#Oj3cr?B=2~vEipD6oKl9yC^Vp1{v|i>CBCcMZVvByy8#g zp32(3+z7lsy(93@X6+VbyPi!RE%dY+6{(EcPKX*3`?lTg1YW@_ac3t$v?!Csm+0MB zLJI8p+p5P}o_EV~G_}PV{)I*@Zxd#mwO|Q4Xcj#T%+Wa9%um#kz$8qlam3^beqqo} zduG4ZM@RUd>k1cmxXJ7hEt z_ds>tTvaF+iZqiK=Xx)FV`%Jjc6dAt(V_JYA!Lt3BprWeYu2bEeJB`meNCeuq(t~X zIvPGp`F&#j0OnfT@7cOgK2dSJh+ziCsPe3KxQ1O;#08ur72Gzd*a$s0+8&g)mY@A) z6=M;7ARnQG*M4EYrXi6+nU}X&6OMrCyqr~KKP-iMG;#gpZ!TKb>iJwW_P}(amcQ?B z)B%D49{IB6h>676nDq*nT#*d@6d(W1fR`bB%FX;)KtX66c}C%_z}nXD8LET(jDhS- z>CTU=Vl<>AG*1B@`ukE}^0D)mxaNh>oBn#4I*^ifRU7lP7>S>qqA%OG#!piT$1PBS zOC0e37;%p9n||@}6i_gz*@f+%cwYS$t=V%&oDL8iu?|LG6VB{OciEV!xMp zRI&;8K0TSe;`akpv#i0IcK5Ni5BTL(JSJ?m-F<};%6SLSC#mc5+il01D=^RDjTysfu<=bpRF@?@6^jxTg{A}lyhfNypOhg$V}~x zUf$0Pn2LJC^7+`+-dDfx=}tc6?^|7Ed37^z*5c!`zcL9~(5FP-3OU@)Dir@H#j%>^ zW_GYU#>c&p7S!g3rZW2SYir^W_#d*ue0CvjSB7#*>Z9BPF#lJv#u+7^ly!{U9P_<{ zdY;#Gvbza-2`>O9B?aPPvf8Nyq>C(*;HDEs?CIuYsnwBQqrH)5DV{94 zq*{L}Y4Lm=g-KM`Ru6yu#=WgE+1?df-gaPiXiHxtWDS!Ih?J&7`-U*JFi?7MYeG{v zjZKWV=KhcP%$Bv)aVSu}9CeB-*gRiV7cNtZ53T z5i#=pGrLY4{UMU&OA*6QS}T&^MTjk*`x_Dsm%+r8m)L=NnRkkE(@O3Wr@Q;6SH-L- zF3h}`pn94=u*-t0aJ+a!{(0`MK9LRbOmL4&;Mk_#8I__zLEj*tRi=-yj6 zHseU(TvdpYp46SWv%x9ff8U3EI(gG*|4qPIX4M6wi92Jx{k^%u?<|_B3B>Cte(eL+ z#aiA%hk!9UMkhlJLIG=#z;0%5v?0Sm$H5WC5$dWA1z-0QT1ib+pb; z3O9nv-dD3-W6Xx=lYvRhKxQDSvs(Tg!4UtUYJ_+x(nO+x_581v$(+7Ap3Ig44OXh0 z)yiIFY1)#N;3^q0hx^=yGy1p0*vOMlD^xk?KqvFBOF(85}AeLZX|&#>MSM{SRY{n^S0Odd$qiufWT@<6g6nzfWVg*cU~ zm Date: Mon, 24 Dec 2018 04:18:46 +0100 Subject: [PATCH 028/384] logo (#9) --- README.md | 2 +- example/pubspec.lock | 2 +- pubspec.yaml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 7c339b65..e31e0773 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ [![pub package](https://img.shields.io/pub/v/flutter_hooks.svg)](https://pub.dartlang.org/packages/flutter_hooks) - + # Flutter Hooks diff --git a/example/pubspec.lock b/example/pubspec.lock index 0c98c2c5..4ccc071b 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -40,7 +40,7 @@ packages: path: ".." relative: true source: path - version: "0.0.1" + version: "0.0.1+2" flutter_test: dependency: "direct dev" description: flutter diff --git a/pubspec.yaml b/pubspec.yaml index 3bd24d10..4f93d0ce 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -3,7 +3,7 @@ description: A flutter implementation of React hooks. It adds a new kind of widg homepage: https://github.com/rrousselGit/flutter_hooks author: Remi Rousselet -version: 0.0.1 +version: 0.0.1+2 environment: sdk: ">=2.0.0-dev.68.0 <3.0.0" From 87159f4192888be2c99aa706faea92f0f7c37976 Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Mon, 24 Dec 2018 08:56:01 +0100 Subject: [PATCH 029/384] v0.1.0 (#10) --- CHANGELOG.md | 8 + README.md | 299 ++++++++++++++++++++++-- example/README.md | 16 -- example/lib/main.dart | 57 ++++- example/pubspec.lock | 10 +- example/pubspec.yaml | 1 + lib/src/hook_impl.dart | 185 +++++++++++---- lib/src/hook_widget.dart | 104 ++++++--- pubspec.yaml | 2 +- test/hook_widget_test.dart | 63 ++++- test/memoized_test.dart | 52 ++--- test/use_animation_controller_test.dart | 41 ++++ test/use_effect_test.dart | 215 +++++++++++++++++ test/use_state_test.dart | 2 +- test/use_stream_controller_test.dart | 84 +++++++ 15 files changed, 995 insertions(+), 144 deletions(-) delete mode 100644 example/README.md create mode 100644 test/use_effect_test.dart create mode 100644 test/use_stream_controller_test.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index 4eec888c..0778861e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +## 0.1.0: + +- `useMemoized` callback doesn't take the previous value anymore (to match React API) +Use `useValueChanged` instead. +- Introduced `useEffect` and `useStreamController` +- fixed a bug where hot-reload while reordering/adding hooks did not work properly +- improved readme + ## 0.0.1: Added a few common hooks: diff --git a/README.md b/README.md index e31e0773..4c044c33 100644 --- a/README.md +++ b/README.md @@ -8,28 +8,39 @@ A flutter implementation of React hooks: https://medium.com/@dan_abramov/making-sense-of-react-hooks-fdbde8803889 -## What are hooks? - Hooks are a new kind of object that manages a `Widget` life-cycles. They exists for one reason: increase the code sharing _between_ widgets and as a complete replacement for `StatefulWidget`. -### The StatefulWidget issue +## Motivation `StatefulWidget` suffer from a big problem: it is very difficult reuse the logic of say `initState` or `dispose`. An obvious example is `AnimationController`: ```dart class Example extends StatefulWidget { + final Duration duration; + + const Example({Key key, @required this.duration}) + : assert(duration != null), + super(key: key); + @override _ExampleState createState() => _ExampleState(); } -class _ExampleState extends State - with SingleTickerProviderStateMixin { +class _ExampleState extends State with SingleTickerProviderStateMixin { AnimationController _controller; @override void initState() { super.initState(); - _controller = AnimationController(vsync: this); + _controller = AnimationController(vsync: this, duration: widget.duration); + } + + @override + void didUpdateWidget(Example oldWidget) { + super.didUpdateWidget(oldWidget); + if (widget.duration != oldWidget.duration) { + _controller.duration = widget.duration; + } } @override @@ -45,26 +56,282 @@ class _ExampleState extends State } ``` -All widgets that desired to use an `AnimationController` will have to copy-paste the `initState`/`dispose`, which is of course undesired. - -Dart mixins can partially solve this issue, but they are the source of another problem: type conflicts. If two mixins defines the same variable, the behavior may vary from a compilation fail to a totally unexpected behavior. +All widgets that desires to use an `AnimationController` will have to reimplement the creation/destruction these life-cycles from scratch, which is of course undesired. -### The Hook solution +Dart mixins can partially solve this issue, but they suffer from other issues: -Hooks are designed so that we can reuse the `initState`/`dispose` logic shown before between widgets. But without the potential issues of a mixin. +- One given mixin can only be used once per class. +- Mixins and the class shares the same type. This means that if two mixins defines a variable under the same name, the end result may vary between compilation fail to unknown behavior. -_Hooks are independents and can be reused as many times as desired._ +--- -This means that with hooks, the equivalent of the previous code is: +Now let's reimplement the previous example using this library: ```dart class Example extends HookWidget { + final Duration duration; + + const Example({Key key, @required this.duration}) + : assert(duration != null), + super(key: key); + @override Widget build(HookContext context) { - final controller = context.useAnimationController( - duration: const Duration(seconds: 1), - ); + final controller = context.useAnimationController(duration: duration); + return Container(); + } +} +``` + +This code is strictly equivalent to the previous example. It still disposes the `AnimationController` and still updates its `duration` when `Example.duration` changes. +But you're probably thinking: + +> Where did all the previous logic go? + +That logic moved into `useAnimationController`. This function is what we call a _Hook_. Hooks have a few specificies: + +- They can be used only in the `build` method of a `HookWidget`. +- The same hook can be reused multiple times without variable conflict. +- Hooks are entirely independent from each others and from the widget. Which means they can easily be extracted into a package and published on [pub](https://pub.dartlang.org/) for others to use. + +## Principle + +Hooks, similarily to `State`, are stored on the `Element` associated to a `Widget`. But instead of having one `State`, the `Element` stores a `List`. Then obtain the content of a `Hook`, one must call `HookContext.use`. + +The hook returned is based on the number of times the `use` method has been called. So that the first call returns the first hook; the second call returns the second hook, the third returns the third hook, ... + +A naive implementation could be the following: + +```dart +class HookElement extends Element { + List _hooks; + int _hookIndex; + + T use(Hook hook) => _hooks[_hookIndex++].build(this); + + @override + performRebuild() { + _hookIndex = 0; + super.performRebuild(); + } +} +``` + +For more explanation of how they are implemented, here's a great article about how they did it in React: https://medium.com/@ryardley/react-hooks-not-magic-just-arrays-cd4f1857236e + +Due to hooks being obtained based on their indexes, there are some rules for using hooks that must be respected: + +### DO call `use` unconditionally + +```dart +Widget build(HookContext context) { + context.use(MyHook()); + // .... +} +``` + +### DON'T call wrap `use` into a condition + +```dart +Widget build(HookContext context) { + if (condition) { + context.use(MyHook()); + } + // .... +} +``` + +--- + +### DO always call all the hooks: + +```dart +Widget build(HookContext context) { + context.use(Hook1()); + context.use(Hook2()); + // .... +} +``` + +### DON'T abort `build` method before all hooks have been called: + +```dart +Widget build(HookContext context) { + context.use(Hook1()); + if (condition) { return Container(); } + context.use(Hook2()); + // .... } ``` + +____ + +### About hot-reload + +Since hooks are obtained based on their index, one may think that hot-reload will break the application. But that is not the case. + +`HookWidget` overrides the default hot-reload behavior to work with hooks. But in certain situations, the state of a Hook may get reset. +Consider the following list of hooks: + +- A() +- B(0) +- C() + +Then consider that after a hot-reload, we edited the parameter of B: + +- A() +- B(42) +- C() + +Here there are no issue. All hooks keeps their states. + +Now consider that we removed B. We now have: + +- A() +- C() + +In this situation, A keeps its state but C gets a hard reset. + +## How to use + +There are two way to create a hook: + +- A function + +Due to hooks composable nature, functions are the most common solution for custom hooks. +They will have their name prefixed by `use` and take a `HookContext` as argument. + +The following defines a custom hook that creates a variable and log its value on the console whenever the value change: + +```dart +ValueNotifier useLoggedState(HookContext context, [T initialData]) { + final result = context.useState(initialData); + context.useValueChanged(result.value, (_, __) { + print(result.value); + }); + return result; +} +``` + +- A class + +When a hook becomes too complex, it is possible to convert it into a class that extends `Hook`, which can then be used using `HookContext.use`. As a class, the hook will look very similar to a `State` and have access to life-cycles and methods such as `initHook`, `dispose` and `setState`. + +It is prefered to use functions over classes whenever possible, and to hide classes under a function. + +The following defines a hook that prints the time a `State` has been alive. + +```dart +class _TimeAlive extends Hook { + const _TimeAlive(); + + @override + _TimeAliveState createState() => _TimeAliveState(); +} + +class _TimeAliveState extends HookState> { + DateTime start; + + @override + void initHook() { + super.initHook(); + start = DateTime.now(); + } + + @override + void build(HookContext context) { + // this hook doesn't create anything nor uses other hooks + } + + @override + void dispose() { + print(DateTime.now().difference(start)); + super.dispose(); + } +} + +``` + +## Existing hooks + +`HookContext` comes with a list of predefined hooks that are commonly used. They can be used directly on the `HookContext` instance. The existing hooks are: + +- useEffect + +Useful to trigger side effects in a widget and dispose objects. It takes a callback and calls it immediatly. That callback may optionally return a function, which will be called when the widget is disposed. + +By default the callback is called on every `build`, but it is possible to override that behavior by passing a list of objects as second parameter. The callback will then be called only when something inside the list has changed. + +The following call to `useEffect` subscribes to a `Stream` and cancel the subscription when the widget is disposed: + +```dart +Stream stream; +context.useEffect(() { + final subscribtion = stream.listen(print); + // This will cancel the subscribtion when the widget is disposed + // or if the callback is called again. + return subscribtion.cancel; + }, + // when the stream change, useEffect will call the callback again. + [stream], +); +``` + +- useState + +Defines + watch a variable and whenever the value change, calls `setState`. + +The following uses `useState` to make a simple counter application: + +```dart +class Counter extends HookWidget { + @override + Widget build(HookContext context) { + final counter = context.useState(0); + + return GestureDetector( + onTap: () => counter.value++, + child: Text(counter.value.toString()), + ); + } +} +``` + +- useMemoized + +Takes a callback that creates a value, call it, and stores its result so that next time, the value is reused. + +By default the callback is called only on the first build. But it is optionally possible to specify a list of objects as second parameter. The callback will then be called again whenever something inside the list has changed. + +The following sample make an http call and return the created `Future` whenever `userId` changes: + +```dart +String userId; +final Future response = context.useMemoized(() { + return http.get('someUrl/$userId'); +}, [userId]); +``` + +- useValueChanged + +Takes a value and a callback, and call the callback whenever the value changed. The callback can optionally return an object, which will be stored and returned as the result of `useValueChanged`. + +The following example implictly starts a tween animation whenever `color` changes: + +```dart +AnimationController controller; +Color color; + +final colorTween = context.useValueChanged( + color, + (Color oldColor, Animation oldAnimation) { + return ColorTween( + begin: oldAnimation?.value ?? oldColor, + end: color, + ).animate(controller..forward(from: 0)); + }, + ) ?? + AlwaysStoppedAnimation(color); +``` diff --git a/example/README.md b/example/README.md deleted file mode 100644 index 6d17fb36..00000000 --- a/example/README.md +++ /dev/null @@ -1,16 +0,0 @@ -# example - -A new Flutter project. - -## Getting Started - -This project is a starting point for a Flutter application. - -A few resources to get you started if this is your first Flutter project: - -- [Lab: Write your first Flutter app](https://flutter.io/docs/get-started/codelab) -- [Cookbook: Useful Flutter samples](https://flutter.io/docs/cookbook) - -For help getting started with Flutter, view our -[online documentation](https://flutter.io/docs), which offers tutorials, -samples, guidance on mobile development, and a full API reference. diff --git a/example/lib/main.dart b/example/lib/main.dart index 91c5736f..5b13f011 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -1,5 +1,9 @@ +// ignore_for_file: omit_local_variable_types +import 'dart:async'; + import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:shared_preferences/shared_preferences.dart'; void main() => runApp(_MyApp()); @@ -18,18 +22,61 @@ class _Counter extends HookWidget { @override Widget build(HookContext context) { - final counter = context.useState(initialData: 0); + StreamController countController = + _useLocalStorageInt(context, 'counter'); return Scaffold( appBar: AppBar( title: const Text('Counter app'), ), body: Center( - child: Text(counter.value.toString()), - ), - floatingActionButton: FloatingActionButton( - onPressed: () => counter.value++, + child: HookBuilder( + builder: (context) { + AsyncSnapshot count = + context.useStream(countController.stream); + + return !count.hasData + // Currently loading value from local storage, or there's an error + ? const CircularProgressIndicator() + : GestureDetector( + onTap: () => countController.add(count.data + 1), + child: Text('You tapped me ${count.data} times'), + ); + }, + ), ), ); } } + +StreamController _useLocalStorageInt( + HookContext context, + String key, { + int defaultValue = 0, +}) { + final controller = context.useStreamController(); + + context + // We define a callback that will be called on first build + // and whenever the controller/key change + ..useEffect(() { + // We listen to the data and push new values to local storage + final sub = controller.stream.listen((data) async { + final prefs = await SharedPreferences.getInstance(); + await prefs.setInt(key, data); + }); + // Unsubscribe when the widget is disposed + // or on controller/key change + return sub.cancel; + }, [controller, key]) + // We load the initial value + ..useEffect(() { + SharedPreferences.getInstance().then((prefs) async { + int valueFromStorage = prefs.getInt(key); + controller.add(valueFromStorage ?? defaultValue); + }).catchError(controller.addError); + // ensure the callback is called only on first build + }, [controller, key]); + + return controller; +} diff --git a/example/pubspec.lock b/example/pubspec.lock index 4ccc071b..ea66da9f 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -40,7 +40,7 @@ packages: path: ".." relative: true source: path - version: "0.0.1+2" + version: "0.1.0" flutter_test: dependency: "direct dev" description: flutter @@ -74,6 +74,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.0.1" + shared_preferences: + dependency: "direct main" + description: + name: shared_preferences + url: "https://pub.dartlang.org" + source: hosted + version: "0.4.3" sky_engine: dependency: transitive description: flutter @@ -137,3 +144,4 @@ packages: version: "2.0.8" sdks: dart: ">=2.0.0 <3.0.0" + flutter: ">=0.1.4 <2.0.0" diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 2feea3d3..a9f91f03 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -10,6 +10,7 @@ dependencies: flutter: sdk: flutter flutter_hooks: 0.0.1 + shared_preferences: ^0.4.3 dev_dependencies: flutter_test: diff --git a/lib/src/hook_impl.dart b/lib/src/hook_impl.dart index 9a9bf2ed..02fbc4b3 100644 --- a/lib/src/hook_impl.dart +++ b/lib/src/hook_impl.dart @@ -1,7 +1,28 @@ part of 'hook.dart'; +bool _areListsEquals(List p1, List p2) { + if (p1 == p2) { + return true; + } + // is one list is null and the other one isn't, or if they have different size + if ((p1 != p2 && (p1 == null || p2 == null)) || p1.length != p2.length) { + return false; + } + + var i1 = p1.iterator; + var i2 = p2.iterator; + while (true) { + if (!i1.moveNext() || !i2.moveNext()) { + return true; + } + if (i1.current != i2.current) { + return false; + } + } +} + class _MemoizedHook extends Hook { - final T Function(T old) valueBuilder; + final T Function() valueBuilder; final List parameters; const _MemoizedHook(this.valueBuilder, {this.parameters = const []}) @@ -18,28 +39,17 @@ class _MemoizedHookState extends HookState> { @override void initHook() { super.initHook(); - value = hook.valueBuilder(null); + value = hook.valueBuilder(); } @override void didUpdateHook(_MemoizedHook oldHook) { super.didUpdateHook(oldHook); - if (hook.parameters != oldHook.parameters && - (hook.parameters.length != oldHook.parameters.length || - _hasDiffWith(oldHook.parameters))) { - value = hook.valueBuilder(value); + if (!_areListsEquals(hook.parameters, oldHook.parameters)) { + value = hook.valueBuilder(); } } - bool _hasDiffWith(List parameters) { - for (var i = 0; i < parameters.length; i++) { - if (parameters[i] != hook.parameters[i]) { - return true; - } - } - return false; - } - @override T build(HookContext context) { return value; @@ -184,6 +194,21 @@ class _AnimationControllerHookState extends HookState { AnimationController _animationController; + @override + void didUpdateHook(_AnimationControllerHook oldHook) { + super.didUpdateHook(oldHook); + if (hook.vsync != oldHook.vsync) { + assert(hook.vsync != null && oldHook.vsync != null, ''' +Switching between controller and uncontrolled vsync is not allowed. +'''); + _animationController.resync(hook.vsync); + } + + if (hook.duration != oldHook.duration) { + _animationController.duration = hook.duration; + } + } + @override AnimationController build(HookContext context) { final vsync = hook.vsync ?? context.useSingleTickerProvider(); @@ -198,20 +223,9 @@ class _AnimationControllerHookState value: hook.initialValue, ); - context - ..useValueChanged(hook.vsync, resync) - ..useValueChanged(hook.duration, duration); return _animationController; } - void resync(_, __) { - _animationController.resync(hook.vsync); - } - - void duration(_, __) { - _animationController.duration = hook.duration; - } - @override void dispose() { super.dispose(); @@ -235,7 +249,6 @@ class _ListenableStateHook extends HookState { hook.listenable.addListener(_listener); } - /// we do it manually instead of using [HookContext.useValueChanged] to win a split second. @override void didUpdateHook(_ListenableHook oldHook) { super.didUpdateHook(oldHook); @@ -425,23 +438,103 @@ class _StreamHookState extends HookState, _StreamHook> { current.inState(ConnectionState.none); } -/// A [HookWidget] that defer its [HookWidget.build] to a callback -class HookBuilder extends HookWidget { - /// The callback used by [HookBuilder] to create a widget. - /// - /// If the passed [HookContext] trigger a rebuild, [builder] will be called again. - /// [builder] must not return `null`. - final Widget Function(HookContext context) builder; - - /// Creates a widget that delegates its build to a callback. - /// - /// The [builder] argument must not be null. - const HookBuilder({ - @required this.builder, - Key key, - }) : assert(builder != null), - super(key: key); - - @override - Widget build(HookContext context) => builder(context); +class _EffectHook extends Hook { + final VoidCallback Function() effect; + final List parameters; + + const _EffectHook(this.effect, [this.parameters]) : assert(effect != null); + + @override + _EffectHookState createState() => _EffectHookState(); +} + +class _EffectHookState extends HookState { + VoidCallback disposer; + + @override + void initHook() { + super.initHook(); + scheduleEffect(); + } + + @override + void didUpdateHook(_EffectHook oldHook) { + super.didUpdateHook(oldHook); + + if (hook.parameters == null || + !_areListsEquals(hook.parameters, oldHook.parameters)) { + if (disposer != null) { + disposer(); + } + scheduleEffect(); + } + } + + @override + void build(HookContext context) {} + + @override + void dispose() { + if (disposer != null) { + disposer(); + } + super.dispose(); + } + + void scheduleEffect() { + disposer = hook.effect(); + } +} + +class _StreamControllerHook extends Hook> { + final bool sync; + final VoidCallback onListen; + final VoidCallback onCancel; + + const _StreamControllerHook({ + this.sync = false, + this.onListen, + this.onCancel, + }); + + @override + _StreamControllerHookState createState() => + _StreamControllerHookState(); +} + +class _StreamControllerHookState + extends HookState, _StreamControllerHook> { + StreamController _controller; + + @override + void initHook() { + super.initHook(); + _controller = StreamController.broadcast( + sync: hook.sync, + onCancel: hook.onCancel, + onListen: hook.onListen, + ); + } + + @override + void didUpdateHook(_StreamControllerHook oldHook) { + super.didUpdateHook(oldHook); + if (oldHook.onListen != hook.onListen) { + _controller.onListen = hook.onListen; + } + if (oldHook.onCancel != hook.onCancel) { + _controller.onCancel = hook.onCancel; + } + } + + @override + StreamController build(HookContext context) { + return _controller; + } + + @override + void dispose() { + _controller.close(); + super.dispose(); + } } diff --git a/lib/src/hook_widget.dart b/lib/src/hook_widget.dart index db097878..c1ebae33 100644 --- a/lib/src/hook_widget.dart +++ b/lib/src/hook_widget.dart @@ -277,18 +277,15 @@ This may happen if the call to `use` is made under some condition. _hooks.remove(_currentHook.current..dispose()); // has to be done after the dispose call hookState = _createHookState(hook); - // compensate for the `_debutHooksIndex++` at the end - _debugHooksIndex--; - _hooks.add(hookState); + _hooks.insert(_debugHooksIndex, hookState); // we move the iterator back to where it was - _currentHook = _hooks.iterator; - for (var i = 0; - i + 2 < _hooks.length && _hooks[i + 2] != hookState; - i++) { + _currentHook = _hooks.iterator..moveNext(); + for (var i = 0; i < _hooks.length && _hooks[i] != hookState; i++) { _currentHook.moveNext(); } } else { + // new hooks have been pushed at the end of the list. hookState = _createHookState(hook); _hooks.add(hookState); @@ -326,20 +323,6 @@ This may happen if the call to `use` is made under some condition. ..initHook(); } - @override - ValueNotifier useState({T initialData}) { - return use(_StateHook(initialData: initialData)); - } - - @override - T useMemoized(T Function(T previousValue) valueBuilder, - {List parameters = const []}) { - return use(_MemoizedHook( - valueBuilder, - parameters: parameters, - )); - } - @override R useValueChanged(T value, R valueChange(T oldValue, R oldResult)) { return use(_ValueChangedHook(value, valueChange)); @@ -397,11 +380,39 @@ This may happen if the call to `use` is made under some condition. TickerProvider useSingleTickerProvider() { return use(const _TickerProviderHook()); } + + @override + void useEffect(VoidCallback Function() effect, [List parameters]) { + use(_EffectHook(effect, parameters)); + } + + @override + T useMemoized(T Function() valueBuilder, [List parameters = const []]) { + return use(_MemoizedHook( + valueBuilder, + parameters: parameters, + )); + } + + @override + ValueNotifier useState([T initialData]) { + return use(_StateHook(initialData: initialData)); + } + + @override + StreamController useStreamController( + {bool sync = false, VoidCallback onListen, VoidCallback onCancel}) { + return use(_StreamControllerHook( + onCancel: onCancel, + onListen: onListen, + sync: sync, + )); + } } /// A [Widget] that can use [Hook] /// -/// It's usage is very similar to [StatelessWidget]: +/// It's usage is very similar to [StatelessWidget]. /// [HookWidget] do not have any life-cycle and implements /// only a [build] method. /// @@ -442,6 +453,27 @@ class _HookWidgetState extends State { } } +/// A [HookWidget] that defer its [HookWidget.build] to a callback +class HookBuilder extends HookWidget { + /// The callback used by [HookBuilder] to create a widget. + /// + /// If the passed [HookContext] trigger a rebuild, [builder] will be called again. + /// [builder] must not return `null`. + final Widget Function(HookContext context) builder; + + /// Creates a widget that delegates its build to a callback. + /// + /// The [builder] argument must not be null. + const HookBuilder({ + @required this.builder, + Key key, + }) : assert(builder != null), + super(key: key); + + @override + Widget build(HookContext context) => builder(context); +} + /// A [BuildContext] that can use a [Hook]. /// /// See also: @@ -457,7 +489,14 @@ abstract class HookContext extends BuildContext { /// See [Hook] for more explanations. R use(Hook hook); - /// Create a mutable value and subscribes to it. + /// A hook for side-effects + /// + /// [useEffect] is called synchronously on every [HookWidget.build], unless + /// [parameters] is specified. In which case [useEffect] is called again only if + /// any value inside [parameters] as changed. + void useEffect(VoidCallback effect(), [List parameters]); + + /// Create value and subscribes to it. /// /// Whenever [ValueNotifier.value] updates, it will mark the caller [HookContext] /// as needing build. @@ -466,9 +505,9 @@ abstract class HookContext extends BuildContext { /// /// See also: /// - /// * [use] - /// * [Hook] - ValueNotifier useState({T initialData}); + /// * [ValueNotifier] + /// * [useStreamController], an alternative to [ValueNotifier] for state. + ValueNotifier useState([T initialData]); /// Create and cache the instance of an object. /// @@ -478,7 +517,7 @@ abstract class HookContext extends BuildContext { /// * [parameters] can be use to specify a list of objects for [useMemoized] to watch. /// So that whenever [operator==] fails on any parameter or if the length of [parameters] changes, /// [valueBuilder] is called again. - T useMemoized(T valueBuilder(T previousValue), {List parameters}); + T useMemoized(T valueBuilder(), [List parameters = const []]); /// Watches a value. /// @@ -515,6 +554,17 @@ abstract class HookContext extends BuildContext { AnimationBehavior animationBehavior = AnimationBehavior.normal, }); + /// Creates a [StreamController] automatically disposed. + /// + /// See also: + /// * [StreamController] + /// * [HookContext.useStream] + StreamController useStreamController({ + bool sync = false, + VoidCallback onListen, + VoidCallback onCancel, + }); + /// Subscribes to a [Listenable] and mark the widget as needing build /// whenever the listener is called. /// diff --git a/pubspec.yaml b/pubspec.yaml index 4f93d0ce..6e7ea49b 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -3,7 +3,7 @@ description: A flutter implementation of React hooks. It adds a new kind of widg homepage: https://github.com/rrousselGit/flutter_hooks author: Remi Rousselet -version: 0.0.1+2 +version: 0.1.0 environment: sdk: ">=2.0.0-dev.68.0 <3.0.0" diff --git a/test/hook_widget_test.dart b/test/hook_widget_test.dart index 60665e29..15dd166f 100644 --- a/test/hook_widget_test.dart +++ b/test/hook_widget_test.dart @@ -258,13 +258,14 @@ void main() { verifyNever(dispose.call()); }); - testWidgets('hot-reload can add hooks', (tester) async { + testWidgets('hot-reload can add hooks at the end of the list', + (tester) async { HookTest hook1; final dispose2 = Func0(); final initHook2 = Func0(); final didUpdateHook2 = Func1(); - final build2 = Func1(); + final build2 = Func1(); when(builder.call(any)).thenAnswer((invocation) { (invocation.positionalArguments[0] as HookContext) @@ -286,7 +287,7 @@ void main() { when(builder.call(any)).thenAnswer((invocation) { (invocation.positionalArguments[0] as HookContext) ..use(createHook()) - ..use(HookTest( + ..use(HookTest( initHook: initHook2, build: build2, didUpdateHook: didUpdateHook2, @@ -309,6 +310,58 @@ void main() { verifyZeroInteractions(dispose2); verifyZeroInteractions(didUpdateHook2); }); + + testWidgets('hot-reload can add hooks in the middle of the list', + (tester) async { + final dispose2 = Func0(); + final initHook2 = Func0(); + final didUpdateHook2 = Func1(); + final build2 = Func1(); + + when(builder.call(any)).thenAnswer((invocation) { + (invocation.positionalArguments[0] as HookContext) + ..use(createHook()); + return Container(); + }); + + await tester.pumpWidget(HookBuilder(builder: builder.call)); + + final HookElement context = find.byType(HookBuilder).evaluate().first; + + verifyInOrder([ + initHook.call(), + build.call(context), + ]); + verifyZeroInteractions(dispose); + verifyZeroInteractions(didUpdateHook); + + when(builder.call(any)).thenAnswer((invocation) { + (invocation.positionalArguments[0] as HookContext) + ..use(HookTest( + initHook: initHook2, + build: build2, + didUpdateHook: didUpdateHook2, + dispose: dispose2, + )) + ..use(createHook()); + return Container(); + }); + + hotReload(tester); + await tester.pump(); + + verifyInOrder([ + dispose.call(), + initHook2.call(), + build2.call(context), + initHook.call(), + build.call(context), + ]); + verifyNoMoreInteractions(didUpdateHook); + verifyNoMoreInteractions(dispose); + verifyZeroInteractions(dispose2); + verifyZeroInteractions(didUpdateHook2); + }); testWidgets('hot-reload can remove hooks', (tester) async { final dispose2 = Func0(); final initHook2 = Func0(); @@ -384,8 +437,8 @@ void main() { (invocation.positionalArguments[0] as HookContext) ..use(hook1 = createHook()) ..use(HookTest(dispose: dispose2)) - ..use(HookTest(dispose: dispose3)) - ..use(HookTest(dispose: dispose4)); + ..use(HookTest(dispose: dispose3)) + ..use(HookTest(dispose: dispose4)); return Container(); }); diff --git a/test/memoized_test.dart b/test/memoized_test.dart index cf8eda61..470146cc 100644 --- a/test/memoized_test.dart +++ b/test/memoized_test.dart @@ -6,7 +6,7 @@ import 'mock.dart'; void main() { final builder = Func1(); final parameterBuilder = Func0(); - final valueBuilder = Func1(); + final valueBuilder = Func0(); tearDown(() { reset(builder); @@ -22,7 +22,7 @@ void main() { expect(tester.takeException(), isAssertionError); await tester.pumpWidget(HookBuilder(builder: (context) { - context.useMemoized((_) {}, parameters: null); + context.useMemoized(() {}, null); return Container(); })); expect(tester.takeException(), isAssertionError); @@ -32,7 +32,7 @@ void main() { (tester) async { int result; - when(valueBuilder.call(null)).thenReturn(42); + when(valueBuilder.call()).thenReturn(42); when(builder.call(any)).thenAnswer((invocation) { final HookContext context = invocation.positionalArguments.single; @@ -42,7 +42,7 @@ void main() { await tester.pumpWidget(HookBuilder(builder: builder.call)); - verify(valueBuilder.call(null)).called(1); + verify(valueBuilder.call()).called(1); verifyNoMoreInteractions(valueBuilder); expect(result, 42); @@ -61,19 +61,19 @@ void main() { (tester) async { int result; - when(valueBuilder.call(null)).thenReturn(0); + when(valueBuilder.call()).thenReturn(0); when(parameterBuilder.call()).thenReturn([]); when(builder.call(any)).thenAnswer((invocation) { final HookContext context = invocation.positionalArguments.single; - result = context.useMemoized(valueBuilder.call, - parameters: parameterBuilder.call()); + result = + context.useMemoized(valueBuilder.call, parameterBuilder.call()); return Container(); }); await tester.pumpWidget(HookBuilder(builder: builder.call)); - verify(valueBuilder.call(null)).called(1); + verify(valueBuilder.call()).called(1); verifyNoMoreInteractions(valueBuilder); expect(result, 0); @@ -87,12 +87,12 @@ void main() { /* Add parameter */ when(parameterBuilder.call()).thenReturn(['foo']); - when(valueBuilder.call(0)).thenReturn(1); + when(valueBuilder.call()).thenReturn(1); await tester.pumpWidget(HookBuilder(builder: builder.call)); expect(result, 1); - verify(valueBuilder.call(0)).called(1); + verify(valueBuilder.call()).called(1); verifyNoMoreInteractions(valueBuilder); /* No change */ @@ -105,12 +105,12 @@ void main() { /* Remove parameter */ when(parameterBuilder.call()).thenReturn([]); - when(valueBuilder.call(1)).thenReturn(2); + when(valueBuilder.call()).thenReturn(2); await tester.pumpWidget(HookBuilder(builder: builder.call)); expect(result, 2); - verify(valueBuilder.call(1)).called(1); + verify(valueBuilder.call()).called(1); verifyNoMoreInteractions(valueBuilder); /* No change */ @@ -132,17 +132,17 @@ void main() { when(builder.call(any)).thenAnswer((invocation) { final HookContext context = invocation.positionalArguments.single; - result = context.useMemoized(valueBuilder.call, - parameters: parameterBuilder.call()); + result = + context.useMemoized(valueBuilder.call, parameterBuilder.call()); return Container(); }); - when(valueBuilder.call(null)).thenReturn(0); + when(valueBuilder.call()).thenReturn(0); when(parameterBuilder.call()).thenReturn(['foo', 42, 24.0]); await tester.pumpWidget(HookBuilder(builder: builder.call)); - verify(valueBuilder.call(null)).called(1); + verify(valueBuilder.call()).called(1); verifyNoMoreInteractions(valueBuilder); expect(result, 0); @@ -156,32 +156,32 @@ void main() { /* reoder */ - when(valueBuilder.call(0)).thenReturn(1); + when(valueBuilder.call()).thenReturn(1); when(parameterBuilder.call()).thenReturn([42, 'foo', 24.0]); await tester.pumpWidget(HookBuilder(builder: builder.call)); - verify(valueBuilder.call(0)).called(1); + verify(valueBuilder.call()).called(1); verifyNoMoreInteractions(valueBuilder); expect(result, 1); - when(valueBuilder.call(1)).thenReturn(2); + when(valueBuilder.call()).thenReturn(2); when(parameterBuilder.call()).thenReturn([42, 24.0, 'foo']); await tester.pumpWidget(HookBuilder(builder: builder.call)); - verify(valueBuilder.call(1)).called(1); + verify(valueBuilder.call()).called(1); verifyNoMoreInteractions(valueBuilder); expect(result, 2); /* value change */ - when(valueBuilder.call(2)).thenReturn(3); + when(valueBuilder.call()).thenReturn(3); when(parameterBuilder.call()).thenReturn([43, 24.0, 'foo']); await tester.pumpWidget(HookBuilder(builder: builder.call)); - verify(valueBuilder.call(2)).called(1); + verify(valueBuilder.call()).called(1); verifyNoMoreInteractions(valueBuilder); expect(result, 3); @@ -210,17 +210,17 @@ void main() { when(builder.call(any)).thenAnswer((invocation) { final HookContext context = invocation.positionalArguments.single; - result = context.useMemoized(valueBuilder.call, - parameters: parameterBuilder.call()); + result = + context.useMemoized(valueBuilder.call, parameterBuilder.call()); return Container(); }); - when(valueBuilder.call(null)).thenReturn(0); + when(valueBuilder.call()).thenReturn(0); when(parameterBuilder.call()).thenReturn(parameters); await tester.pumpWidget(HookBuilder(builder: builder.call)); - verify(valueBuilder.call(null)).called(1); + verify(valueBuilder.call()).called(1); verifyNoMoreInteractions(valueBuilder); expect(result, 0); diff --git a/test/use_animation_controller_test.dart b/test/use_animation_controller_test.dart index 96a1e676..29a60bd6 100644 --- a/test/use_animation_controller_test.dart +++ b/test/use_animation_controller_test.dart @@ -103,6 +103,47 @@ void main() { // dispose await tester.pumpWidget(const SizedBox()); }); + + testWidgets('switch between controlled and uncontrolled throws', + (tester) async { + await tester.pumpWidget(HookBuilder( + builder: (context) { + context.useAnimationController(); + return Container(); + }, + )); + + await expectPump( + () => tester.pumpWidget(HookBuilder( + builder: (context) { + context.useAnimationController(vsync: tester); + return Container(); + }, + )), + throwsAssertionError, + ); + + await tester.pumpWidget(Container()); + + + // the other way around + await tester.pumpWidget(HookBuilder( + builder: (context) { + context.useAnimationController(vsync: tester); + return Container(); + }, + )); + + await expectPump( + () => tester.pumpWidget(HookBuilder( + builder: (context) { + context.useAnimationController(); + return Container(); + }, + )), + throwsAssertionError, + ); + }); } class _TickerProvider extends Mock implements TickerProvider {} diff --git a/test/use_effect_test.dart b/test/use_effect_test.dart new file mode 100644 index 00000000..509dce6e --- /dev/null +++ b/test/use_effect_test.dart @@ -0,0 +1,215 @@ +import 'package:flutter/widgets.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; + +import 'mock.dart'; + +final effect = Func0(); +final unrelated = Func0(); +List parameters; + +Widget builder() => HookBuilder(builder: (context) { + context.useEffect(effect.call, parameters); + unrelated.call(); + return Container(); + }); + +void main() { + tearDown(() { + parameters = null; + reset(unrelated); + reset(effect); + }); + testWidgets('useEffect calls callback on every build', (tester) async { + final effect = Func0(); + final unrelated = Func0(); + + builder() => HookBuilder(builder: (context) { + context.useEffect(effect.call); + unrelated.call(); + return Container(); + }); + + await tester.pumpWidget(builder()); + + verifyInOrder([ + effect.call(), + unrelated.call(), + ]); + verifyNoMoreInteractions(effect); + + await tester.pumpWidget(builder()); + + verifyInOrder([ + effect.call(), + unrelated.call(), + ]); + verifyNoMoreInteractions(effect); + }); + + testWidgets( + 'useEffect with parameters calls callback when changing from null to something', + (tester) async { + await tester.pumpWidget(builder()); + + verifyInOrder([ + effect.call(), + unrelated.call(), + ]); + verifyNoMoreInteractions(effect); + + parameters = ['foo']; + await tester.pumpWidget(builder()); + + verifyInOrder([ + effect.call(), + unrelated.call(), + ]); + verifyNoMoreInteractions(effect); + }); + + testWidgets('useEffect adding parameters call callback', (tester) async { + parameters = ['foo']; + await tester.pumpWidget(builder()); + + verifyInOrder([ + effect.call(), + unrelated.call(), + ]); + verifyNoMoreInteractions(effect); + + parameters = ['foo', 42]; + await tester.pumpWidget(builder()); + + verifyInOrder([ + effect.call(), + unrelated.call(), + ]); + verifyNoMoreInteractions(effect); + }); + + testWidgets('useEffect removing parameters call callback', (tester) async { + parameters = ['foo']; + await tester.pumpWidget(builder()); + + verifyInOrder([ + effect.call(), + unrelated.call(), + ]); + verifyNoMoreInteractions(effect); + + parameters = []; + await tester.pumpWidget(builder()); + + verifyInOrder([ + effect.call(), + unrelated.call(), + ]); + verifyNoMoreInteractions(effect); + }); + testWidgets('useEffect changing parameters call callback', (tester) async { + parameters = ['foo']; + await tester.pumpWidget(builder()); + + verifyInOrder([ + effect.call(), + unrelated.call(), + ]); + verifyNoMoreInteractions(effect); + + parameters = ['bar']; + await tester.pumpWidget(builder()); + + verifyInOrder([ + effect.call(), + unrelated.call(), + ]); + verifyNoMoreInteractions(effect); + }); + testWidgets( + 'useEffect with same parameters but different arrays don t call callback', + (tester) async { + parameters = ['foo']; + await tester.pumpWidget(builder()); + + verifyInOrder([ + effect.call(), + unrelated.call(), + ]); + verifyNoMoreInteractions(effect); + + parameters = ['foo']; + await tester.pumpWidget(builder()); + + verifyNoMoreInteractions(effect); + }); + testWidgets( + 'useEffect with same array but different parameters don t call callback', + (tester) async { + parameters = ['foo']; + await tester.pumpWidget(builder()); + + verifyInOrder([ + effect.call(), + unrelated.call(), + ]); + verifyNoMoreInteractions(effect); + + parameters.add('bar'); + await tester.pumpWidget(builder()); + + verifyNoMoreInteractions(effect); + }); + + testWidgets('useEffect disposer called whenever callback called', + (tester) async { + final effect = Func0(); + List parameters; + + builder() => HookBuilder(builder: (context) { + context.useEffect(effect.call, parameters); + return Container(); + }); + + parameters = ['foo']; + final disposerA = Func0(); + when(effect.call()).thenReturn(disposerA); + + await tester.pumpWidget(builder()); + + verify(effect.call()).called(1); + verifyNoMoreInteractions(effect); + verifyZeroInteractions(disposerA); + + await tester.pumpWidget(builder()); + + verifyNoMoreInteractions(effect); + verifyZeroInteractions(disposerA); + + parameters = ['bar']; + final disposerB = Func0(); + when(effect.call()).thenReturn(disposerB); + + await tester.pumpWidget(builder()); + + verifyInOrder([ + disposerA.call(), + effect.call(), + ]); + verifyNoMoreInteractions(disposerA); + verifyNoMoreInteractions(effect); + verifyZeroInteractions(disposerB); + + await tester.pumpWidget(builder()); + + verifyNoMoreInteractions(disposerA); + verifyNoMoreInteractions(effect); + verifyZeroInteractions(disposerB); + + await tester.pumpWidget(Container()); + + verify(disposerB.call()).called(1); + verifyNoMoreInteractions(disposerB); + verifyNoMoreInteractions(disposerA); + verifyNoMoreInteractions(effect); + }); +} diff --git a/test/use_state_test.dart b/test/use_state_test.dart index b5d830e1..e2ceed29 100644 --- a/test/use_state_test.dart +++ b/test/use_state_test.dart @@ -11,7 +11,7 @@ void main() { await tester.pumpWidget(HookBuilder( builder: (context) { element = context as HookElement; - state = context.useState(initialData: 42); + state = context.useState(42); return Container(); }, )); diff --git a/test/use_stream_controller_test.dart b/test/use_stream_controller_test.dart new file mode 100644 index 00000000..3a67e87b --- /dev/null +++ b/test/use_stream_controller_test.dart @@ -0,0 +1,84 @@ +import 'dart:async'; + +import 'package:flutter/widgets.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; + +import 'mock.dart'; + +void main() { + group('useStreamController', () { + testWidgets('basics', (tester) async { + StreamController controller; + + await tester.pumpWidget(HookBuilder(builder: (context) { + controller = context.useStreamController(); + return Container(); + })); + + expect(controller, isNot(isInstanceOf())); + expect(controller.onListen, isNull); + expect(controller.onCancel, isNull); + expect(() => controller.onPause, throwsUnsupportedError); + expect(() => controller.onResume, throwsUnsupportedError); + + final previousController = controller; + final onListen = () {}; + final onCancel = () {}; + await tester.pumpWidget(HookBuilder(builder: (context) { + controller = context.useStreamController( + sync: true, + onCancel: onCancel, + onListen: onListen, + ); + return Container(); + })); + + expect(controller, previousController); + expect(controller, isNot(isInstanceOf())); + expect(controller.onListen, onListen); + expect(controller.onCancel, onCancel); + expect(() => controller.onPause, throwsUnsupportedError); + expect(() => controller.onResume, throwsUnsupportedError); + + await tester.pumpWidget(Container()); + + expect(controller.isClosed, true); + }); + testWidgets('sync', (tester) async { + StreamController controller; + + await tester.pumpWidget(HookBuilder(builder: (context) { + controller = context.useStreamController(sync: true); + return Container(); + })); + + expect(controller, isInstanceOf()); + expect(controller.onListen, isNull); + expect(controller.onCancel, isNull); + expect(() => controller.onPause, throwsUnsupportedError); + expect(() => controller.onResume, throwsUnsupportedError); + + final previousController = controller; + final onListen = () {}; + final onCancel = () {}; + await tester.pumpWidget(HookBuilder(builder: (context) { + controller = context.useStreamController( + onCancel: onCancel, + onListen: onListen, + ); + return Container(); + })); + + expect(controller, previousController); + expect(controller, isInstanceOf()); + expect(controller.onListen, onListen); + expect(controller.onCancel, onCancel); + expect(() => controller.onPause, throwsUnsupportedError); + expect(() => controller.onResume, throwsUnsupportedError); + + await tester.pumpWidget(Container()); + + expect(controller.isClosed, true); + }); + }); +} From e2652ecb35ca20ad97f72b79a49c88d8680ccce4 Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Mon, 24 Dec 2018 11:12:04 +0100 Subject: [PATCH 030/384] Fix readme (#11) --- README.md | 86 ++++++++++++++++++++++++++++++++++--------------------- 1 file changed, 54 insertions(+), 32 deletions(-) diff --git a/README.md b/README.md index 4c044c33..61d9547f 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,4 @@ -[![Build Status](https://travis-ci.org/rrousselGit/flutter_hooks.svg?branch=master)](https://travis-ci.org/rrousselGit/flutter_hooks) [![codecov](https://codecov.io/gh/rrousselGit/flutter_hooks/branch/master/graph/badge.svg)](https://codecov.io/gh/rrousselGit/flutter_hooks) - -[![pub package](https://img.shields.io/pub/v/flutter_hooks.svg)](https://pub.dartlang.org/packages/flutter_hooks) +[![Build Status](https://travis-ci.org/rrousselGit/flutter_hooks.svg?branch=master)](https://travis-ci.org/rrousselGit/flutter_hooks) [![codecov](https://codecov.io/gh/rrousselGit/flutter_hooks/branch/master/graph/badge.svg)](https://codecov.io/gh/rrousselGit/flutter_hooks) [![pub package](https://img.shields.io/pub/v/flutter_hooks.svg)](https://pub.dartlang.org/packages/flutter_hooks) [![pub package](https://img.shields.io/badge/Awesome-Flutter-blue.svg?longCache=true&style=flat-square)](https://stackoverflow.com/questions/tagged/flutter?sort=votes) @@ -8,11 +6,11 @@ A flutter implementation of React hooks: https://medium.com/@dan_abramov/making-sense-of-react-hooks-fdbde8803889 -Hooks are a new kind of object that manages a `Widget` life-cycles. They exists for one reason: increase the code sharing _between_ widgets and as a complete replacement for `StatefulWidget`. +Hooks are a new kind of object that manages a `Widget` life-cycles. They exist for one reason: increase the code sharing _between_ widgets and as a complete replacement for `StatefulWidget`. ## Motivation -`StatefulWidget` suffer from a big problem: it is very difficult reuse the logic of say `initState` or `dispose`. An obvious example is `AnimationController`: +`StatefulWidget` suffer from a big problem: it is very difficult to reuse the logic of say `initState` or `dispose`. An obvious example is `AnimationController`: ```dart class Example extends StatefulWidget { @@ -56,12 +54,12 @@ class _ExampleState extends State with SingleTickerProviderStateMixin { } ``` -All widgets that desires to use an `AnimationController` will have to reimplement the creation/destruction these life-cycles from scratch, which is of course undesired. +All widgets that desire to use an `AnimationController` will have to reimplement the creation/destruction these life-cycles from scratch, which is of course undesired. Dart mixins can partially solve this issue, but they suffer from other issues: -- One given mixin can only be used once per class. -- Mixins and the class shares the same type. This means that if two mixins defines a variable under the same name, the end result may vary between compilation fail to unknown behavior. +- One given mixin can only be used once per class. +- Mixins and the class shares the same type. This means that if two mixins define a variable under the same name, the end result may vary between compilation fail to unknown behavior. --- @@ -88,15 +86,15 @@ But you're probably thinking: > Where did all the previous logic go? -That logic moved into `useAnimationController`. This function is what we call a _Hook_. Hooks have a few specificies: +That logic moved into `useAnimationController`, a function shipped with this library (see https://github.com/rrousselGit/flutter_hooks#existing-hooks). This function is what we call a _Hook_. Hooks have a few specificities: -- They can be used only in the `build` method of a `HookWidget`. -- The same hook can be reused multiple times without variable conflict. -- Hooks are entirely independent from each others and from the widget. Which means they can easily be extracted into a package and published on [pub](https://pub.dartlang.org/) for others to use. +- They can be used only in the `build` method of a `HookWidget`. +- The same hook can be reused multiple times without variable conflict. +- Hooks are entirely independent of each other and from the widget. Which means they can easily be extracted into a package and published on [pub](https://pub.dartlang.org/) for others to use. ## Principle -Hooks, similarily to `State`, are stored on the `Element` associated to a `Widget`. But instead of having one `State`, the `Element` stores a `List`. Then obtain the content of a `Hook`, one must call `HookContext.use`. +Hooks, similarily to `State`, are stored on the `Element` associated with a `Widget`. But instead of having one `State`, the `Element` stores a `List`. Then obtain the content of a `Hook`, one must call `HookContext.use`. The hook returned is based on the number of times the `use` method has been called. So that the first call returns the first hook; the second call returns the second hook, the third returns the third hook, ... @@ -130,7 +128,7 @@ Widget build(HookContext context) { } ``` -### DON'T call wrap `use` into a condition +### DON'T wrap `use` into a condition ```dart Widget build(HookContext context) { @@ -153,7 +151,7 @@ Widget build(HookContext context) { } ``` -### DON'T abort `build` method before all hooks have been called: +### DON'T aborts `build` method before all hooks have been called: ```dart Widget build(HookContext context) { @@ -166,7 +164,7 @@ Widget build(HookContext context) { } ``` -____ +--- ### About hot-reload @@ -185,7 +183,7 @@ Then consider that after a hot-reload, we edited the parameter of B: - B(42) - C() -Here there are no issue. All hooks keeps their states. +Here there is no issue. All hooks keep their states. Now consider that we removed B. We now have: @@ -196,14 +194,14 @@ In this situation, A keeps its state but C gets a hard reset. ## How to use -There are two way to create a hook: +There is two way to create a hook: -- A function +- A function Due to hooks composable nature, functions are the most common solution for custom hooks. -They will have their name prefixed by `use` and take a `HookContext` as argument. +They will have their name prefixed by `use` and take a `HookContext` as an argument. -The following defines a custom hook that creates a variable and log its value on the console whenever the value change: +The following defines a custom hook that creates a variable and logs its value on the console whenever the value change: ```dart ValueNotifier useLoggedState(HookContext context, [T initialData]) { @@ -215,11 +213,11 @@ ValueNotifier useLoggedState(HookContext context, [T initialData]) { } ``` -- A class +- A class When a hook becomes too complex, it is possible to convert it into a class that extends `Hook`, which can then be used using `HookContext.use`. As a class, the hook will look very similar to a `State` and have access to life-cycles and methods such as `initHook`, `dispose` and `setState`. -It is prefered to use functions over classes whenever possible, and to hide classes under a function. +It is preferred to use functions over classes whenever possible and to hide classes under a function. The following defines a hook that prints the time a `State` has been alive. @@ -258,11 +256,11 @@ class _TimeAliveState extends HookState> { `HookContext` comes with a list of predefined hooks that are commonly used. They can be used directly on the `HookContext` instance. The existing hooks are: -- useEffect +- useEffect -Useful to trigger side effects in a widget and dispose objects. It takes a callback and calls it immediatly. That callback may optionally return a function, which will be called when the widget is disposed. +Useful to trigger side effects in a widget and dispose objects. It takes a callback and calls it immediately. That callback may optionally return a function, which will be called when the widget is disposed. -By default the callback is called on every `build`, but it is possible to override that behavior by passing a list of objects as second parameter. The callback will then be called only when something inside the list has changed. +By default, the callback is called on every `build`, but it is possible to override that behavior by passing a list of objects as the second parameter. The callback will then be called only when something inside the list has changed. The following call to `useEffect` subscribes to a `Stream` and cancel the subscription when the widget is disposed: @@ -279,7 +277,7 @@ context.useEffect(() { ); ``` -- useState +- useState Defines + watch a variable and whenever the value change, calls `setState`. @@ -299,11 +297,11 @@ class Counter extends HookWidget { } ``` -- useMemoized +- useMemoized -Takes a callback that creates a value, call it, and stores its result so that next time, the value is reused. +Takes a callback that creates a value, calls it, and stores its result so that next time, the value is reused. -By default the callback is called only on the first build. But it is optionally possible to specify a list of objects as second parameter. The callback will then be called again whenever something inside the list has changed. +By default, the callback is called only on the first build. But it is optionally possible to specify a list of objects as the second parameter. The callback will then be called again whenever something inside the list has changed. The following sample make an http call and return the created `Future` whenever `userId` changes: @@ -314,11 +312,11 @@ final Future response = context.useMemoized(() { }, [userId]); ``` -- useValueChanged +- useValueChanged Takes a value and a callback, and call the callback whenever the value changed. The callback can optionally return an object, which will be stored and returned as the result of `useValueChanged`. -The following example implictly starts a tween animation whenever `color` changes: +The following example implicitly starts a tween animation whenever `color` changes: ```dart AnimationController controller; @@ -335,3 +333,27 @@ final colorTween = context.useValueChanged( ) ?? AlwaysStoppedAnimation(color); ``` + +- useAnimationController, useStreamController, useSingleTickerProvider + +A set of hooks that handles the whole life-cycle of an object. These hooks will take care of both creating, disposing and updating the object. + +They are the equivalent of both `initState`, `dispose` and `didUpdateWidget` for that specific object. + +```dart +Duration duration; +AnimationController controller = context.useAnimationController( + // duration is automatically updates when the widget is rebuilt with a different `duration` + duration: duration, +); +``` + +- useStream, useFuture, useAnimation, useValueListenable, useListenable + +A set of hooks that subscribes to an object and calls `setState` accordingly. + +```dart +Stream stream; +// automatically rebuild the widget when a new value is pushed to the stream +AsyncSnapshot snapshot = context.useStream(stream); +``` From 716208641062adb6f4277fcc28fff0d9b9de1c8b Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Mon, 24 Dec 2018 12:22:11 +0100 Subject: [PATCH 031/384] Readme2 (#12) --- README.md | 98 +++++++++++++++++++++++++++++++++++-------------------- 1 file changed, 62 insertions(+), 36 deletions(-) diff --git a/README.md b/README.md index 61d9547f..5d670989 100644 --- a/README.md +++ b/README.md @@ -54,16 +54,16 @@ class _ExampleState extends State with SingleTickerProviderStateMixin { } ``` -All widgets that desire to use an `AnimationController` will have to reimplement the creation/destruction these life-cycles from scratch, which is of course undesired. +All widgets that desire to use an `AnimationController` will have to reimplement almost of all this from scratch, which is of course undesired. -Dart mixins can partially solve this issue, but they suffer from other issues: +Dart mixins can partially solve this issue, but they suffer from other problems: -- One given mixin can only be used once per class. -- Mixins and the class shares the same type. This means that if two mixins define a variable under the same name, the end result may vary between compilation fail to unknown behavior. +- A given mixin can only be used once per class. +- Mixins and the class shares the same object. This means that if two mixins define a variable under the same name, the end result may vary between compilation fail to unknown behavior. --- -Now let's reimplement the previous example using this library: +This library propose a third solution: ```dart class Example extends HookWidget { @@ -84,21 +84,33 @@ class Example extends HookWidget { This code is strictly equivalent to the previous example. It still disposes the `AnimationController` and still updates its `duration` when `Example.duration` changes. But you're probably thinking: -> Where did all the previous logic go? +> Where did all the logic go? -That logic moved into `useAnimationController`, a function shipped with this library (see https://github.com/rrousselGit/flutter_hooks#existing-hooks). This function is what we call a _Hook_. Hooks have a few specificities: +That logic moved into `useAnimationController`, a function included directly in this library (see https://github.com/rrousselGit/flutter_hooks#existing-hooks). It is what we call a _Hook_. + +Hooks are a new kind of objects with some specificities: + +- They can only be used in the `build` method of a `HookWidget`. +- The same hook is reusable an infinite number of times + The following code defines two independent `AnimationController`, and they are correctly preserved when the widget rebuild. + +```dart +Widget build(HookContext context) { + final controller = context.useAnimationController(); + final controller2 = context.useAnimationController(); + return Container(); +} +``` -- They can be used only in the `build` method of a `HookWidget`. -- The same hook can be reused multiple times without variable conflict. - Hooks are entirely independent of each other and from the widget. Which means they can easily be extracted into a package and published on [pub](https://pub.dartlang.org/) for others to use. ## Principle -Hooks, similarily to `State`, are stored on the `Element` associated with a `Widget`. But instead of having one `State`, the `Element` stores a `List`. Then obtain the content of a `Hook`, one must call `HookContext.use`. +Similarily to `State`, hooks are stored on the `Element` of a `Widget`. But instead of having one `State`, the `Element` stores a `List`. Then to use a `Hook`, one must call `HookContext.use`. -The hook returned is based on the number of times the `use` method has been called. So that the first call returns the first hook; the second call returns the second hook, the third returns the third hook, ... +The hook returned by `use` is based on the number of times it has been called. The first call returns the first hook; the second call returns the second hook, the third returns the third hook, ... -A naive implementation could be the following: +If this is still unclear, a naive implementation of hooks is the following: ```dart class HookElement extends Element { @@ -117,7 +129,9 @@ class HookElement extends Element { For more explanation of how they are implemented, here's a great article about how they did it in React: https://medium.com/@ryardley/react-hooks-not-magic-just-arrays-cd4f1857236e -Due to hooks being obtained based on their indexes, there are some rules for using hooks that must be respected: +## Rules + +Due to hooks being obtained from their index, there are some rules that must be respected: ### DO call `use` unconditionally @@ -168,40 +182,47 @@ Widget build(HookContext context) { ### About hot-reload -Since hooks are obtained based on their index, one may think that hot-reload will break the application. But that is not the case. +Since hooks are obtained from their index, one may think that hot-reload while refactoring will break the application. + +But worry not, `HookWidget` overrides the default hot-reload behavior to work with hooks. Still, there are some situations in which the state of a Hook may get reset. -`HookWidget` overrides the default hot-reload behavior to work with hooks. But in certain situations, the state of a Hook may get reset. Consider the following list of hooks: -- A() -- B(0) -- C() +```dart +context.use(HookA()); +context.use(HookB(0)); +context.use(HookC(0)); +``` -Then consider that after a hot-reload, we edited the parameter of B: +Then consider that after a hot-reload, we edited the parameter of `HookB`: -- A() -- B(42) -- C() +```dart +context.use(HookA()); +context.use(HookB(42)); +context.use(HookC()); +``` -Here there is no issue. All hooks keep their states. +Here everything works fine; all hooks keep their states. -Now consider that we removed B. We now have: +Now consider that we removed `HookB`. We now have: -- A() -- C() +```dart +context.use(HookA()); +context.use(HookC()); +``` -In this situation, A keeps its state but C gets a hard reset. +In this situation, `HookA` keeps its state but `HookC` gets a hard reset. +This happens because when a refactoring is done, all hooks _after_ the first line impacted are disposed. Since `HookC` was placed after `HookB`, is got disposed. ## How to use -There is two way to create a hook: +There are two ways to create a hook: - A function -Due to hooks composable nature, functions are the most common solution for custom hooks. -They will have their name prefixed by `use` and take a `HookContext` as an argument. +Functions is by far the most common way to write a hook. Thanks to hooks being composable by nature, a function will be able to combine other hooks to create a custom hook. By convention these functions will be prefixed by `use`. -The following defines a custom hook that creates a variable and logs its value on the console whenever the value change: +The following defines a custom hook that creates a variable and logs its value on the console whenever the value changes: ```dart ValueNotifier useLoggedState(HookContext context, [T initialData]) { @@ -215,9 +236,13 @@ ValueNotifier useLoggedState(HookContext context, [T initialData]) { - A class -When a hook becomes too complex, it is possible to convert it into a class that extends `Hook`, which can then be used using `HookContext.use`. As a class, the hook will look very similar to a `State` and have access to life-cycles and methods such as `initHook`, `dispose` and `setState`. +When a hook becomes too complex, it is possible to convert it into a class that extends `Hook`, which can then be used using `HookContext.use`. As a class, the hook will look very similar to a `State` and have access to life-cycles and methods such as `initHook`, `dispose` and `setState`. It is usually a good practice to hide the class under a function as such: -It is preferred to use functions over classes whenever possible and to hide classes under a function. +```dart +Result useMyHook(HookContext context) { + return context.use(_MyHook()); +} +``` The following defines a hook that prints the time a `State` has been alive. @@ -281,7 +306,7 @@ context.useEffect(() { Defines + watch a variable and whenever the value change, calls `setState`. -The following uses `useState` to make a simple counter application: +The following code uses `useState` to make a counter application: ```dart class Counter extends HookWidget { @@ -290,6 +315,7 @@ class Counter extends HookWidget { final counter = context.useState(0); return GestureDetector( + // automatically triggers a rebuild of Counter widget onTap: () => counter.value++, child: Text(counter.value.toString()), ); @@ -299,11 +325,11 @@ class Counter extends HookWidget { - useMemoized -Takes a callback that creates a value, calls it, and stores its result so that next time, the value is reused. +Takes a callback, calls it synchronously and returns its result. The result is then stored to that subsequent calls will return the same result without calling the callback. By default, the callback is called only on the first build. But it is optionally possible to specify a list of objects as the second parameter. The callback will then be called again whenever something inside the list has changed. -The following sample make an http call and return the created `Future` whenever `userId` changes: +The following sample make an http call and return the created `Future`. And if `userId` changes, a new call will be made: ```dart String userId; From 13d771259475f4ad11ef4941cdaa6d7e8fd009e6 Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Mon, 24 Dec 2018 15:39:18 +0100 Subject: [PATCH 032/384] fix awesome link (#13) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 5d670989..590493fd 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -[![Build Status](https://travis-ci.org/rrousselGit/flutter_hooks.svg?branch=master)](https://travis-ci.org/rrousselGit/flutter_hooks) [![codecov](https://codecov.io/gh/rrousselGit/flutter_hooks/branch/master/graph/badge.svg)](https://codecov.io/gh/rrousselGit/flutter_hooks) [![pub package](https://img.shields.io/pub/v/flutter_hooks.svg)](https://pub.dartlang.org/packages/flutter_hooks) [![pub package](https://img.shields.io/badge/Awesome-Flutter-blue.svg?longCache=true&style=flat-square)](https://stackoverflow.com/questions/tagged/flutter?sort=votes) +[![Build Status](https://travis-ci.org/rrousselGit/flutter_hooks.svg?branch=master)](https://travis-ci.org/rrousselGit/flutter_hooks) [![codecov](https://codecov.io/gh/rrousselGit/flutter_hooks/branch/master/graph/badge.svg)](https://codecov.io/gh/rrousselGit/flutter_hooks) [![pub package](https://img.shields.io/pub/v/flutter_hooks.svg)](https://pub.dartlang.org/packages/flutter_hooks) [![pub package](https://img.shields.io/badge/Awesome-Flutter-blue.svg?longCache=true&style=flat-square)](https://github.com/Solido/awesome-flutter) From 1a5f49715dacb5fff667a72c26cf368499369c25 Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Wed, 26 Dec 2018 11:59:59 +0100 Subject: [PATCH 033/384] bumb version --- CHANGELOG.md | 4 +++- example/pubspec.lock | 2 +- pubspec.yaml | 4 ++-- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0778861e..a4e9b22a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,9 @@ +## 0.2.0: + ## 0.1.0: - `useMemoized` callback doesn't take the previous value anymore (to match React API) -Use `useValueChanged` instead. + Use `useValueChanged` instead. - Introduced `useEffect` and `useStreamController` - fixed a bug where hot-reload while reordering/adding hooks did not work properly - improved readme diff --git a/example/pubspec.lock b/example/pubspec.lock index ea66da9f..ca6c8c53 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -40,7 +40,7 @@ packages: path: ".." relative: true source: path - version: "0.1.0" + version: "0.2.0" flutter_test: dependency: "direct dev" description: flutter diff --git a/pubspec.yaml b/pubspec.yaml index 6e7ea49b..5b7b4b89 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -3,7 +3,7 @@ description: A flutter implementation of React hooks. It adds a new kind of widg homepage: https://github.com/rrousselGit/flutter_hooks author: Remi Rousselet -version: 0.1.0 +version: 0.2.0 environment: sdk: ">=2.0.0-dev.68.0 <3.0.0" @@ -15,5 +15,5 @@ dependencies: dev_dependencies: flutter_test: sdk: flutter - pedantic: 1.4.0 + pedantic: ">=1.4.0 <2.0.0" mockito: ">=4.0.0 <5.0.0" From 0e368db8dc27136e75ee53729248052bf03c25c1 Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Wed, 26 Dec 2018 19:26:05 +0100 Subject: [PATCH 034/384] Keys #14 (#18) Add support for keys. --- .travis.yml | 2 +- CHANGELOG.md | 3 + analysis_options.yaml | 6 +- example/lib/main.dart | 9 +- lib/src/hook_impl.dart | 59 ++---- lib/src/hook_widget.dart | 112 ++++++++--- pubspec.yaml | 2 +- test/hook_widget_test.dart | 248 +++++++++++++++++++++++- test/memoized_test.dart | 20 +- test/mock.dart | 10 +- test/use_animation_controller_test.dart | 24 ++- test/use_effect_test.dart | 39 ++-- test/use_stream_controller_test.dart | 16 ++ test/use_ticker_provider_test.dart | 20 ++ 14 files changed, 460 insertions(+), 110 deletions(-) diff --git a/.travis.yml b/.travis.yml index 1dc7a077..e823856a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -14,7 +14,7 @@ script: - set -e - flutter packages get - flutter format --set-exit-if-changed lib example - - flutter analyze --no-current-package lib example + - flutter analyze lib example - flutter test --no-pub --coverage # export coverage - bash <(curl -s https://codecov.io/bash) diff --git a/CHANGELOG.md b/CHANGELOG.md index a4e9b22a..94b0f474 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ ## 0.2.0: +- Introduced keys for hooks and applied them to hooks where it makes sense. +- fixes a bug where hot-reload without using hooks throwed an exception + ## 0.1.0: - `useMemoized` callback doesn't take the previous value anymore (to match React API) diff --git a/analysis_options.yaml b/analysis_options.yaml index 233a180f..7579a070 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -4,10 +4,8 @@ analyzer: implicit-casts: false implicit-dynamic: false errors: - strong_mode_implicit_dynamic_list_literal: ignore - strong_mode_implicit_dynamic_map_literal: ignore - strong_mode_implicit_dynamic_parameter: ignore - strong_mode_implicit_dynamic_variable: ignore + todo: error + include_file_not_found: ignore linter: rules: - public_member_api_docs diff --git a/example/lib/main.dart b/example/lib/main.dart index 5b13f011..e35a79a5 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -24,7 +24,6 @@ class _Counter extends HookWidget { Widget build(HookContext context) { StreamController countController = _useLocalStorageInt(context, 'counter'); - return Scaffold( appBar: AppBar( title: const Text('Counter app'), @@ -40,7 +39,7 @@ class _Counter extends HookWidget { ? const CircularProgressIndicator() : GestureDetector( onTap: () => countController.add(count.data + 1), - child: Text('You tapped me ${count.data} times'), + child: Text('You tapped me ${count.data} times.'), ); }, ), @@ -54,7 +53,7 @@ StreamController _useLocalStorageInt( String key, { int defaultValue = 0, }) { - final controller = context.useStreamController(); + final controller = context.useStreamController(keys: [key]); context // We define a callback that will be called on first build @@ -68,7 +67,7 @@ StreamController _useLocalStorageInt( // Unsubscribe when the widget is disposed // or on controller/key change return sub.cancel; - }, [controller, key]) + }, [controller, key]) // We load the initial value ..useEffect(() { SharedPreferences.getInstance().then((prefs) async { @@ -76,7 +75,7 @@ StreamController _useLocalStorageInt( controller.add(valueFromStorage ?? defaultValue); }).catchError(controller.addError); // ensure the callback is called only on first build - }, [controller, key]); + }, [controller, key]); return controller; } diff --git a/lib/src/hook_impl.dart b/lib/src/hook_impl.dart index 02fbc4b3..8c07ebae 100644 --- a/lib/src/hook_impl.dart +++ b/lib/src/hook_impl.dart @@ -1,33 +1,12 @@ part of 'hook.dart'; -bool _areListsEquals(List p1, List p2) { - if (p1 == p2) { - return true; - } - // is one list is null and the other one isn't, or if they have different size - if ((p1 != p2 && (p1 == null || p2 == null)) || p1.length != p2.length) { - return false; - } - - var i1 = p1.iterator; - var i2 = p2.iterator; - while (true) { - if (!i1.moveNext() || !i2.moveNext()) { - return true; - } - if (i1.current != i2.current) { - return false; - } - } -} - class _MemoizedHook extends Hook { final T Function() valueBuilder; - final List parameters; - const _MemoizedHook(this.valueBuilder, {this.parameters = const []}) + const _MemoizedHook(this.valueBuilder, {List keys = const []}) : assert(valueBuilder != null), - assert(parameters != null); + assert(keys != null), + super(keys: keys); @override _MemoizedHookState createState() => _MemoizedHookState(); @@ -42,14 +21,6 @@ class _MemoizedHookState extends HookState> { value = hook.valueBuilder(); } - @override - void didUpdateHook(_MemoizedHook oldHook) { - super.didUpdateHook(oldHook); - if (!_areListsEquals(hook.parameters, oldHook.parameters)) { - value = hook.valueBuilder(); - } - } - @override T build(HookContext context) { return value; @@ -120,7 +91,7 @@ class _StateHookState extends HookState, _StateHook> { } class _TickerProviderHook extends Hook { - const _TickerProviderHook(); + const _TickerProviderHook([List keys]) : super(keys: keys); @override _TickerProviderHookState createState() => _TickerProviderHookState(); @@ -183,7 +154,8 @@ class _AnimationControllerHook extends Hook { this.upperBound, this.vsync, this.animationBehavior, - }); + List keys, + }) : super(keys: keys); @override _AnimationControllerHookState createState() => @@ -211,7 +183,8 @@ Switching between controller and uncontrolled vsync is not allowed. @override AnimationController build(HookContext context) { - final vsync = hook.vsync ?? context.useSingleTickerProvider(); + final vsync = + hook.vsync ?? context.useSingleTickerProvider(keys: hook.keys); _animationController ??= AnimationController( vsync: vsync, @@ -440,9 +413,10 @@ class _StreamHookState extends HookState, _StreamHook> { class _EffectHook extends Hook { final VoidCallback Function() effect; - final List parameters; - const _EffectHook(this.effect, [this.parameters]) : assert(effect != null); + const _EffectHook(this.effect, [List keys]) + : assert(effect != null), + super(keys: keys); @override _EffectHookState createState() => _EffectHookState(); @@ -461,8 +435,7 @@ class _EffectHookState extends HookState { void didUpdateHook(_EffectHook oldHook) { super.didUpdateHook(oldHook); - if (hook.parameters == null || - !_areListsEquals(hook.parameters, oldHook.parameters)) { + if (hook.keys == null) { if (disposer != null) { disposer(); } @@ -491,11 +464,9 @@ class _StreamControllerHook extends Hook> { final VoidCallback onListen; final VoidCallback onCancel; - const _StreamControllerHook({ - this.sync = false, - this.onListen, - this.onCancel, - }); + const _StreamControllerHook( + {this.sync = false, this.onListen, this.onCancel, List keys}) + : super(keys: keys); @override _StreamControllerHookState createState() => diff --git a/lib/src/hook_widget.dart b/lib/src/hook_widget.dart index c1ebae33..4e26de5d 100644 --- a/lib/src/hook_widget.dart +++ b/lib/src/hook_widget.dart @@ -112,7 +112,45 @@ part of 'hook.dart'; @immutable abstract class Hook { /// Allows subclasses to have a `const` constructor - const Hook(); + const Hook({this.keys}); + + /// A list of objects that specify if a [HookState] should be reused or a new one should be created. + /// + /// When a new [Hook] is created, the framework checks if keys matches using [Hook.shouldPreserveState]. + /// If they don't, the previously created [HookState] is disposed, and a new one is created + /// using [Hook.createState], followed by [HookState.initHook]. + final List keys; + + /// The algorithm to determine if a [HookState] should be reused or disposed. + /// + /// This compares [Hook.keys] to see if they contains any difference. + /// A state is preserved when: + /// + /// - `hook1.keys == hook2.keys` (typically if the list is immutable) + /// - If there's any difference in the content of [Hook.keys], using `operator==`. + static bool shouldPreserveState(Hook hook1, Hook hook2) { + final p1 = hook1.keys; + final p2 = hook2.keys; + + if (p1 == p2) { + return true; + } + // is one list is null and the other one isn't, or if they have different size + if ((p1 != p2 && (p1 == null || p2 == null)) || p1.length != p2.length) { + return false; + } + + var i1 = p1.iterator; + var i2 = p2.iterator; + while (true) { + if (!i1.moveNext() || !i2.moveNext()) { + return true; + } + if (i1.current != i2.current) { + return false; + } + } + } /// Creates the mutable state for this hook linked to its widget creator. /// @@ -172,7 +210,7 @@ abstract class HookState> { /// An [Element] that uses a [HookWidget] as its configuration. class HookElement extends StatefulElement implements HookContext { Iterator _currentHook; - int _debugHooksIndex; + int _hookIndex; List _hooks; bool _debugIsBuilding; @@ -191,9 +229,9 @@ class HookElement extends StatefulElement implements HookContext { _currentHook = _hooks?.iterator; // first iterator always has null _currentHook?.moveNext(); + _hookIndex = 0; assert(() { _debugShouldDispose = false; - _debugHooksIndex = 0; _isFirstBuild ??= true; _didReassemble ??= false; _debugIsBuilding = true; @@ -204,17 +242,17 @@ class HookElement extends StatefulElement implements HookContext { // dispose removed items assert(() { if (_didReassemble) { - while (_currentHook.current != null) { + while (_currentHook?.current != null) { _currentHook.current.dispose(); _currentHook.moveNext(); - _debugHooksIndex++; + _hookIndex++; } } return true; }()); - assert(_debugHooksIndex == (_hooks?.length ?? 0), ''' + assert(_hookIndex == (_hooks?.length ?? 0), ''' Build for $widget finished with less hooks used than a previous build. -Used $_debugHooksIndex hooks while a previous build had ${_hooks.length}. +Used $_hookIndex hooks while a previous build had ${_hooks.length}. This may happen if the call to `use` is made under some condition. '''); @@ -277,7 +315,7 @@ This may happen if the call to `use` is made under some condition. _hooks.remove(_currentHook.current..dispose()); // has to be done after the dispose call hookState = _createHookState(hook); - _hooks.insert(_debugHooksIndex, hookState); + _hooks.insert(_hookIndex, hookState); // we move the iterator back to where it was _currentHook = _hooks.iterator..moveNext(); @@ -299,20 +337,30 @@ This may happen if the call to `use` is made under some condition. }()); assert(_currentHook.current?.hook?.runtimeType == hook.runtimeType); - hookState = _currentHook.current as HookState>; - _currentHook.moveNext(); - - if (hookState._hook != hook) { + if (_currentHook.current.hook == hook) { + hookState = _currentHook.current as HookState>; + _currentHook.moveNext(); + } else if (Hook.shouldPreserveState(_currentHook.current.hook, hook)) { + hookState = _currentHook.current as HookState>; + _currentHook.moveNext(); final previousHook = hookState._hook; hookState .._hook = hook ..didUpdateHook(previousHook); + } else { + _hooks.removeAt(_hookIndex).dispose(); + hookState = _createHookState(hook); + _hooks.insert(_hookIndex, hookState); + + // we move the iterator back to where it was + _currentHook = _hooks.iterator..moveNext(); + for (var i = 0; i < _hooks.length && _hooks[i] != hookState; i++) { + _currentHook.moveNext(); + } + _currentHook.moveNext(); } } - assert(() { - _debugHooksIndex++; - return true; - }()); + _hookIndex++; return hookState.build(this); } @@ -337,6 +385,7 @@ This may happen if the call to `use` is made under some condition. double upperBound = 1, TickerProvider vsync, AnimationBehavior animationBehavior = AnimationBehavior.normal, + List keys, }) { return use(_AnimationControllerHook( duration: duration, @@ -346,6 +395,7 @@ This may happen if the call to `use` is made under some condition. upperBound: upperBound, vsync: vsync, animationBehavior: animationBehavior, + keys: keys, )); } @@ -377,20 +427,22 @@ This may happen if the call to `use` is made under some condition. } @override - TickerProvider useSingleTickerProvider() { - return use(const _TickerProviderHook()); + TickerProvider useSingleTickerProvider({List keys}) { + return use( + keys != null ? _TickerProviderHook(keys) : const _TickerProviderHook(), + ); } @override - void useEffect(VoidCallback Function() effect, [List parameters]) { - use(_EffectHook(effect, parameters)); + void useEffect(VoidCallback Function() effect, [List keys]) { + use(_EffectHook(effect, keys)); } @override - T useMemoized(T Function() valueBuilder, [List parameters = const []]) { + T useMemoized(T Function() valueBuilder, [List keys = const []]) { return use(_MemoizedHook( valueBuilder, - parameters: parameters, + keys: keys, )); } @@ -401,11 +453,15 @@ This may happen if the call to `use` is made under some condition. @override StreamController useStreamController( - {bool sync = false, VoidCallback onListen, VoidCallback onCancel}) { + {bool sync = false, + VoidCallback onListen, + VoidCallback onCancel, + List keys}) { return use(_StreamControllerHook( onCancel: onCancel, onListen: onListen, sync: sync, + keys: keys, )); } } @@ -514,10 +570,10 @@ abstract class HookContext extends BuildContext { /// [useMemoized] will immediatly call [valueBuilder] on first call and store its result. /// Later calls to [useMemoized] will reuse the created instance. /// - /// * [parameters] can be use to specify a list of objects for [useMemoized] to watch. - /// So that whenever [operator==] fails on any parameter or if the length of [parameters] changes, + /// * [keys] can be use to specify a list of objects for [useMemoized] to watch. + /// So that whenever [operator==] fails on any parameter or if the length of [keys] changes, /// [valueBuilder] is called again. - T useMemoized(T valueBuilder(), [List parameters = const []]); + T useMemoized(T valueBuilder(), [List keys = const []]); /// Watches a value. /// @@ -529,7 +585,7 @@ abstract class HookContext extends BuildContext { /// /// See also: /// * [SingleTickerProviderStateMixin] - TickerProvider useSingleTickerProvider(); + TickerProvider useSingleTickerProvider({List keys}); /// Creates an [AnimationController] automatically disposed. /// @@ -552,6 +608,7 @@ abstract class HookContext extends BuildContext { double upperBound = 1, TickerProvider vsync, AnimationBehavior animationBehavior = AnimationBehavior.normal, + List keys, }); /// Creates a [StreamController] automatically disposed. @@ -563,6 +620,7 @@ abstract class HookContext extends BuildContext { bool sync = false, VoidCallback onListen, VoidCallback onCancel, + List keys, }); /// Subscribes to a [Listenable] and mark the widget as needing build diff --git a/pubspec.yaml b/pubspec.yaml index 5b7b4b89..688c6e73 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -15,5 +15,5 @@ dependencies: dev_dependencies: flutter_test: sdk: flutter - pedantic: ">=1.4.0 <2.0.0" + pedantic: ^1.4.0 mockito: ">=4.0.0 <5.0.0" diff --git a/test/hook_widget_test.dart b/test/hook_widget_test.dart index 15dd166f..34407278 100644 --- a/test/hook_widget_test.dart +++ b/test/hook_widget_test.dart @@ -27,6 +27,143 @@ void main() { reset(didUpdateHook); }); + testWidgets('hooks can be disposed independently with keys', (tester) async { + List keys; + List keys2; + + final dispose2 = Func0(); + when(builder.call(any)).thenAnswer((invocation) { + (invocation.positionalArguments[0] as HookContext) + ..use(HookTest(dispose: dispose.call, keys: keys)) + ..use(HookTest(dispose: dispose2.call, keys: keys2)); + return Container(); + }); + await tester.pumpWidget(HookBuilder(builder: builder.call)); + + verifyZeroInteractions(dispose); + verifyZeroInteractions(dispose2); + + keys = []; + await tester.pumpWidget(HookBuilder(builder: builder.call)); + + verify(dispose.call()).called(1); + verifyZeroInteractions(dispose2); + + keys2 = []; + await tester.pumpWidget(HookBuilder(builder: builder.call)); + + verify(dispose2.call()).called(1); + verifyNoMoreInteractions(dispose); + }); + testWidgets('keys recreate hookstate', (tester) async { + List keys; + + final createState = Func0>(); + when(createState.call()).thenReturn(HookStateTest()); + + when(builder.call(any)).thenAnswer((invocation) { + (invocation.positionalArguments[0] as HookContext) + ..use(HookTest( + build: build.call, + dispose: dispose.call, + didUpdateHook: didUpdateHook.call, + initHook: initHook.call, + keys: keys, + createStateFn: createState.call, + )); + return Container(); + }); + await tester.pumpWidget(HookBuilder(builder: builder.call)); + + final HookElement context = find.byType(HookBuilder).evaluate().first; + + verifyInOrder([ + createState.call(), + initHook.call(), + build.call(context), + ]); + verifyNoMoreInteractions(createState); + verifyNoMoreInteractions(initHook); + verifyNoMoreInteractions(build); + verifyNoMoreInteractions(didUpdateHook); + verifyNoMoreInteractions(dispose); + + await tester.pumpWidget(HookBuilder(builder: builder.call)); + + verifyInOrder([ + didUpdateHook.call(any), + build.call(context), + ]); + verifyNoMoreInteractions(createState); + verifyNoMoreInteractions(initHook); + verifyNoMoreInteractions(build); + verifyNoMoreInteractions(didUpdateHook); + verifyNoMoreInteractions(dispose); + + // from null to array + keys = []; + await tester.pumpWidget(HookBuilder(builder: builder.call)); + + verifyInOrder([ + dispose.call(), + createState.call(), + initHook.call(), + build.call(context) + ]); + verifyNoMoreInteractions(createState); + verifyNoMoreInteractions(initHook); + verifyNoMoreInteractions(build); + verifyNoMoreInteractions(didUpdateHook); + verifyNoMoreInteractions(dispose); + + // array immutable + keys.add(42); + + await tester.pumpWidget(HookBuilder(builder: builder.call)); + + verifyInOrder([ + didUpdateHook.call(any), + build.call(context), + ]); + verifyNoMoreInteractions(createState); + verifyNoMoreInteractions(initHook); + verifyNoMoreInteractions(build); + verifyNoMoreInteractions(didUpdateHook); + verifyNoMoreInteractions(dispose); + + // new array but content equal + keys = [42]; + + await tester.pumpWidget(HookBuilder(builder: builder.call)); + + verifyInOrder([ + didUpdateHook.call(any), + build.call(context), + ]); + verifyNoMoreInteractions(createState); + verifyNoMoreInteractions(initHook); + verifyNoMoreInteractions(build); + verifyNoMoreInteractions(didUpdateHook); + verifyNoMoreInteractions(dispose); + + // new array new content + keys = [44]; + + await tester.pumpWidget(HookBuilder(builder: builder.call)); + + verifyInOrder([ + dispose.call(), + createState.call(), + initHook.call(), + build.call(context) + ]); + verifyNoMoreInteractions(createState); + verifyNoMoreInteractions(initHook); + verifyNoMoreInteractions(build); + verifyNoMoreInteractions(didUpdateHook); + verifyNoMoreInteractions(dispose); + }); + testWidgets('hook & setState', (tester) async { final setState = Func0(); final hook = MyHook(); @@ -319,8 +456,7 @@ void main() { final build2 = Func1(); when(builder.call(any)).thenAnswer((invocation) { - (invocation.positionalArguments[0] as HookContext) - ..use(createHook()); + (invocation.positionalArguments[0] as HookContext)..use(createHook()); return Container(); }); @@ -511,6 +647,114 @@ void main() { verifyZeroInteractions(didUpdateHook3); verifyZeroInteractions(didUpdateHook4); }); + + testWidgets('hot-reload disposes hooks when type change', (tester) async { + HookTest hook1; + + final dispose2 = Func0(); + final initHook2 = Func0(); + final didUpdateHook2 = Func1(); + final build2 = Func1(); + + final dispose3 = Func0(); + final initHook3 = Func0(); + final didUpdateHook3 = Func1(); + final build3 = Func1(); + + final dispose4 = Func0(); + final initHook4 = Func0(); + final didUpdateHook4 = Func1(); + final build4 = Func1(); + + when(builder.call(any)).thenAnswer((invocation) { + (invocation.positionalArguments[0] as HookContext) + ..use(hook1 = createHook()) + ..use(HookTest(dispose: dispose2)) + ..use(HookTest(dispose: dispose3)) + ..use(HookTest(dispose: dispose4)); + return Container(); + }); + + await tester.pumpWidget(HookBuilder(builder: builder.call)); + + final HookElement context = find.byType(HookBuilder).evaluate().first; + + // We don't care about datas of the first render + clearInteractions(initHook); + clearInteractions(didUpdateHook); + clearInteractions(dispose); + clearInteractions(build); + + clearInteractions(initHook2); + clearInteractions(didUpdateHook2); + clearInteractions(dispose2); + clearInteractions(build2); + + clearInteractions(initHook3); + clearInteractions(didUpdateHook3); + clearInteractions(dispose3); + clearInteractions(build3); + + clearInteractions(initHook4); + clearInteractions(didUpdateHook4); + clearInteractions(dispose4); + clearInteractions(build4); + + when(builder.call(any)).thenAnswer((invocation) { + (invocation.positionalArguments[0] as HookContext) + ..use(createHook()) + // changed type from HookTest + ..use(HookTest( + initHook: initHook2, + build: build2, + didUpdateHook: didUpdateHook2, + )) + ..use(HookTest( + initHook: initHook3, + build: build3, + didUpdateHook: didUpdateHook3, + )) + ..use(HookTest( + initHook: initHook4, + build: build4, + didUpdateHook: didUpdateHook4, + )); + return Container(); + }); + + hotReload(tester); + await tester.pump(); + + verifyInOrder([ + didUpdateHook.call(hook1), + build.call(context), + dispose2.call(), + initHook2.call(), + build2.call(context), + dispose3.call(), + initHook3.call(), + build3.call(context), + dispose4.call(), + initHook4.call(), + build4.call(context), + ]); + verifyZeroInteractions(initHook); + verifyZeroInteractions(dispose); + verifyZeroInteractions(didUpdateHook2); + verifyZeroInteractions(didUpdateHook3); + verifyZeroInteractions(didUpdateHook4); + }); + + testWidgets('hot-reload without hooks do not crash', (tester) async { + when(builder.call(any)).thenAnswer((invocation) { + return Container(); + }); + + await tester.pumpWidget(HookBuilder(builder: builder.call)); + + hotReload(tester); + await expectPump(() => tester.pump(), completes); + }); } class MyHook extends Hook { diff --git a/test/memoized_test.dart b/test/memoized_test.dart index 470146cc..c137fa12 100644 --- a/test/memoized_test.dart +++ b/test/memoized_test.dart @@ -62,7 +62,7 @@ void main() { int result; when(valueBuilder.call()).thenReturn(0); - when(parameterBuilder.call()).thenReturn([]); + when(parameterBuilder.call()).thenReturn([]); when(builder.call(any)).thenAnswer((invocation) { final HookContext context = invocation.positionalArguments.single; @@ -86,7 +86,7 @@ void main() { /* Add parameter */ - when(parameterBuilder.call()).thenReturn(['foo']); + when(parameterBuilder.call()).thenReturn(['foo']); when(valueBuilder.call()).thenReturn(1); await tester.pumpWidget(HookBuilder(builder: builder.call)); @@ -104,7 +104,7 @@ void main() { /* Remove parameter */ - when(parameterBuilder.call()).thenReturn([]); + when(parameterBuilder.call()).thenReturn([]); when(valueBuilder.call()).thenReturn(2); await tester.pumpWidget(HookBuilder(builder: builder.call)); @@ -138,7 +138,7 @@ void main() { }); when(valueBuilder.call()).thenReturn(0); - when(parameterBuilder.call()).thenReturn(['foo', 42, 24.0]); + when(parameterBuilder.call()).thenReturn(['foo', 42, 24.0]); await tester.pumpWidget(HookBuilder(builder: builder.call)); @@ -148,7 +148,7 @@ void main() { /* Array reference changed but content didn't */ - when(parameterBuilder.call()).thenReturn(['foo', 42, 24.0]); + when(parameterBuilder.call()).thenReturn(['foo', 42, 24.0]); await tester.pumpWidget(HookBuilder(builder: builder.call)); verifyNoMoreInteractions(valueBuilder); @@ -157,7 +157,7 @@ void main() { /* reoder */ when(valueBuilder.call()).thenReturn(1); - when(parameterBuilder.call()).thenReturn([42, 'foo', 24.0]); + when(parameterBuilder.call()).thenReturn([42, 'foo', 24.0]); await tester.pumpWidget(HookBuilder(builder: builder.call)); @@ -166,7 +166,7 @@ void main() { expect(result, 1); when(valueBuilder.call()).thenReturn(2); - when(parameterBuilder.call()).thenReturn([42, 24.0, 'foo']); + when(parameterBuilder.call()).thenReturn([42, 24.0, 'foo']); await tester.pumpWidget(HookBuilder(builder: builder.call)); @@ -177,7 +177,7 @@ void main() { /* value change */ when(valueBuilder.call()).thenReturn(3); - when(parameterBuilder.call()).thenReturn([43, 24.0, 'foo']); + when(parameterBuilder.call()).thenReturn([43, 24.0, 'foo']); await tester.pumpWidget(HookBuilder(builder: builder.call)); @@ -188,7 +188,7 @@ void main() { /* Comparison is done using operator== */ // type change - when(parameterBuilder.call()).thenReturn([43.0, 24.0, 'foo']); + when(parameterBuilder.call()).thenReturn([43.0, 24.0, 'foo']); await tester.pumpWidget(HookBuilder(builder: builder.call)); @@ -206,7 +206,7 @@ void main() { 'memoized parameter reference do not change don\'t call valueBuilder', (tester) async { int result; - final parameters = []; + final parameters = []; when(builder.call(any)).thenAnswer((invocation) { final HookContext context = invocation.positionalArguments.single; diff --git a/test/mock.dart b/test/mock.dart index d28a056c..12c0faa8 100644 --- a/test/mock.dart +++ b/test/mock.dart @@ -30,16 +30,20 @@ class HookTest extends Hook { final void Function() dispose; final void Function() initHook; final void Function(HookTest previousHook) didUpdateHook; + final HookStateTest Function() createStateFn; HookTest({ this.build, this.dispose, this.initHook, this.didUpdateHook, - }) : super(); + this.createStateFn, + List keys, + }) : super(keys: keys); @override - HookStateTest createState() => HookStateTest(); + HookStateTest createState() => + createStateFn != null ? createStateFn() : HookStateTest(); } class HookStateTest extends HookState> { @@ -62,7 +66,7 @@ class HookStateTest extends HookState> { @override void didUpdateHook(HookTest oldHook) { super.didUpdateHook(oldHook); - if (hook.dispose != null) { + if (hook.didUpdateHook != null) { hook.didUpdateHook(oldHook); } } diff --git a/test/use_animation_controller_test.dart b/test/use_animation_controller_test.dart index 29a60bd6..5d31fbee 100644 --- a/test/use_animation_controller_test.dart +++ b/test/use_animation_controller_test.dart @@ -125,7 +125,6 @@ void main() { await tester.pumpWidget(Container()); - // the other way around await tester.pumpWidget(HookBuilder( builder: (context) { @@ -144,6 +143,29 @@ void main() { throwsAssertionError, ); }); + + testWidgets('useAnimationController pass down keys', (tester) async { + List keys; + AnimationController controller; + await tester.pumpWidget(HookBuilder( + builder: (context) { + controller = context.useAnimationController(keys: keys); + return Container(); + }, + )); + + final previous = controller; + keys = []; + + await tester.pumpWidget(HookBuilder( + builder: (context) { + controller = context.useAnimationController(keys: keys); + return Container(); + }, + )); + + expect(previous, isNot(controller)); + }); } class _TickerProvider extends Mock implements TickerProvider {} diff --git a/test/use_effect_test.dart b/test/use_effect_test.dart index 509dce6e..7b00106a 100644 --- a/test/use_effect_test.dart +++ b/test/use_effect_test.dart @@ -19,10 +19,22 @@ void main() { reset(unrelated); reset(effect); }); + testWidgets('useEffect null callback throws', (tester) async { + await expectPump( + () => tester.pumpWidget(HookBuilder(builder: (c) { + c.useEffect(null); + return Container(); + })), + throwsAssertionError, + ); + }); testWidgets('useEffect calls callback on every build', (tester) async { final effect = Func0(); final unrelated = Func0(); + final dispose = Func0(); + when(effect.call()).thenReturn(dispose.call); + builder() => HookBuilder(builder: (context) { context.useEffect(effect.call); unrelated.call(); @@ -35,14 +47,17 @@ void main() { effect.call(), unrelated.call(), ]); + verifyNoMoreInteractions(dispose); verifyNoMoreInteractions(effect); await tester.pumpWidget(builder()); verifyInOrder([ + dispose.call(), effect.call(), unrelated.call(), ]); + verifyNoMoreInteractions(dispose); verifyNoMoreInteractions(effect); }); @@ -57,7 +72,7 @@ void main() { ]); verifyNoMoreInteractions(effect); - parameters = ['foo']; + parameters = ['foo']; await tester.pumpWidget(builder()); verifyInOrder([ @@ -68,7 +83,7 @@ void main() { }); testWidgets('useEffect adding parameters call callback', (tester) async { - parameters = ['foo']; + parameters = ['foo']; await tester.pumpWidget(builder()); verifyInOrder([ @@ -77,7 +92,7 @@ void main() { ]); verifyNoMoreInteractions(effect); - parameters = ['foo', 42]; + parameters = ['foo', 42]; await tester.pumpWidget(builder()); verifyInOrder([ @@ -88,7 +103,7 @@ void main() { }); testWidgets('useEffect removing parameters call callback', (tester) async { - parameters = ['foo']; + parameters = ['foo']; await tester.pumpWidget(builder()); verifyInOrder([ @@ -97,7 +112,7 @@ void main() { ]); verifyNoMoreInteractions(effect); - parameters = []; + parameters = []; await tester.pumpWidget(builder()); verifyInOrder([ @@ -107,7 +122,7 @@ void main() { verifyNoMoreInteractions(effect); }); testWidgets('useEffect changing parameters call callback', (tester) async { - parameters = ['foo']; + parameters = ['foo']; await tester.pumpWidget(builder()); verifyInOrder([ @@ -116,7 +131,7 @@ void main() { ]); verifyNoMoreInteractions(effect); - parameters = ['bar']; + parameters = ['bar']; await tester.pumpWidget(builder()); verifyInOrder([ @@ -128,7 +143,7 @@ void main() { testWidgets( 'useEffect with same parameters but different arrays don t call callback', (tester) async { - parameters = ['foo']; + parameters = ['foo']; await tester.pumpWidget(builder()); verifyInOrder([ @@ -137,7 +152,7 @@ void main() { ]); verifyNoMoreInteractions(effect); - parameters = ['foo']; + parameters = ['foo']; await tester.pumpWidget(builder()); verifyNoMoreInteractions(effect); @@ -145,7 +160,7 @@ void main() { testWidgets( 'useEffect with same array but different parameters don t call callback', (tester) async { - parameters = ['foo']; + parameters = ['foo']; await tester.pumpWidget(builder()); verifyInOrder([ @@ -170,7 +185,7 @@ void main() { return Container(); }); - parameters = ['foo']; + parameters = ['foo']; final disposerA = Func0(); when(effect.call()).thenReturn(disposerA); @@ -185,7 +200,7 @@ void main() { verifyNoMoreInteractions(effect); verifyZeroInteractions(disposerA); - parameters = ['bar']; + parameters = ['bar']; final disposerB = Func0(); when(effect.call()).thenReturn(disposerB); diff --git a/test/use_stream_controller_test.dart b/test/use_stream_controller_test.dart index 3a67e87b..97124e49 100644 --- a/test/use_stream_controller_test.dart +++ b/test/use_stream_controller_test.dart @@ -7,6 +7,22 @@ import 'mock.dart'; void main() { group('useStreamController', () { + testWidgets('keys', (tester) async { + StreamController controller; + + await tester.pumpWidget(HookBuilder(builder: (context) { + controller = context.useStreamController(); + return Container(); + })); + + final previous = controller; + await tester.pumpWidget(HookBuilder(builder: (context) { + controller = context.useStreamController(keys: []); + return Container(); + })); + + expect(previous, isNot(controller)); + }); testWidgets('basics', (tester) async { StreamController controller; diff --git a/test/use_ticker_provider_test.dart b/test/use_ticker_provider_test.dart index 8a57f234..a0ef4b35 100644 --- a/test/use_ticker_provider_test.dart +++ b/test/use_ticker_provider_test.dart @@ -60,4 +60,24 @@ void main() { animationController.dispose(); } }); + + testWidgets('useSingleTickerProvider pass down keys', (tester) async { + TickerProvider provider; + List keys; + + await tester.pumpWidget(HookBuilder(builder: (context) { + provider = context.useSingleTickerProvider(keys: keys); + return Container(); + })); + + final previousProvider = provider; + keys = []; + + await tester.pumpWidget(HookBuilder(builder: (context) { + provider = context.useSingleTickerProvider(keys: keys); + return Container(); + })); + + expect(previousProvider, isNot(provider)); + }); } From e0e7852ce3b3190eddaba53b04aec2d0d06e75b6 Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Wed, 26 Dec 2018 23:11:25 +0100 Subject: [PATCH 035/384] Remove HookContext (#20) --- README.md | 78 ++--- example/lib/main.dart | 50 ++- lib/src/hook_impl.dart | 23 +- lib/src/hook_widget.dart | 419 +++++++++++------------- test/hook_builder_test.dart | 2 +- test/hook_widget_test.dart | 247 +++++++------- test/memoized_test.dart | 21 +- test/mock.dart | 4 +- test/use_animation_controller_test.dart | 18 +- test/use_animation_test.dart | 4 +- test/use_effect_test.dart | 8 +- test/use_future_test.dart | 4 +- test/use_listenable_test.dart | 4 +- test/use_state_test.dart | 4 +- test/use_stream_controller_test.dart | 12 +- test/use_stream_test.dart | 4 +- test/use_ticker_provider_test.dart | 10 +- test/use_value_changed_test.dart | 26 +- test/use_value_listenable_test.dart | 4 +- 19 files changed, 433 insertions(+), 509 deletions(-) diff --git a/README.md b/README.md index 590493fd..45efacc1 100644 --- a/README.md +++ b/README.md @@ -74,8 +74,8 @@ class Example extends HookWidget { super(key: key); @override - Widget build(HookContext context) { - final controller = context.useAnimationController(duration: duration); + Widget build(BuildContext context) { + final controller = useAnimationController(duration: duration); return Container(); } } @@ -95,9 +95,9 @@ Hooks are a new kind of objects with some specificities: The following code defines two independent `AnimationController`, and they are correctly preserved when the widget rebuild. ```dart -Widget build(HookContext context) { - final controller = context.useAnimationController(); - final controller2 = context.useAnimationController(); +Widget build(BuildContext context) { + final controller = useAnimationController(); + final controller2 = useAnimationController(); return Container(); } ``` @@ -106,7 +106,7 @@ Widget build(HookContext context) { ## Principle -Similarily to `State`, hooks are stored on the `Element` of a `Widget`. But instead of having one `State`, the `Element` stores a `List`. Then to use a `Hook`, one must call `HookContext.use`. +Similarily to `State`, hooks are stored on the `Element` of a `Widget`. But instead of having one `State`, the `Element` stores a `List`. Then to use a `Hook`, one must call `Hook.use`. The hook returned by `use` is based on the number of times it has been called. The first call returns the first hook; the second call returns the second hook, the third returns the third hook, ... @@ -136,8 +136,8 @@ Due to hooks being obtained from their index, there are some rules that must be ### DO call `use` unconditionally ```dart -Widget build(HookContext context) { - context.use(MyHook()); +Widget build(BuildContext context) { + Hook.use(MyHook()); // .... } ``` @@ -145,9 +145,9 @@ Widget build(HookContext context) { ### DON'T wrap `use` into a condition ```dart -Widget build(HookContext context) { +Widget build(BuildContext context) { if (condition) { - context.use(MyHook()); + Hook.use(MyHook()); } // .... } @@ -158,9 +158,9 @@ Widget build(HookContext context) { ### DO always call all the hooks: ```dart -Widget build(HookContext context) { - context.use(Hook1()); - context.use(Hook2()); +Widget build(BuildContext context) { + Hook.use(Hook1()); + Hook.use(Hook2()); // .... } ``` @@ -168,12 +168,12 @@ Widget build(HookContext context) { ### DON'T aborts `build` method before all hooks have been called: ```dart -Widget build(HookContext context) { - context.use(Hook1()); +Widget build(BuildContext context) { + Hook.use(Hook1()); if (condition) { return Container(); } - context.use(Hook2()); + Hook.use(Hook2()); // .... } ``` @@ -189,17 +189,17 @@ But worry not, `HookWidget` overrides the default hot-reload behavior to work wi Consider the following list of hooks: ```dart -context.use(HookA()); -context.use(HookB(0)); -context.use(HookC(0)); +Hook.use(HookA()); +Hook.use(HookB(0)); +Hook.use(HookC(0)); ``` Then consider that after a hot-reload, we edited the parameter of `HookB`: ```dart -context.use(HookA()); -context.use(HookB(42)); -context.use(HookC()); +Hook.use(HookA()); +Hook.use(HookB(42)); +Hook.use(HookC()); ``` Here everything works fine; all hooks keep their states. @@ -207,8 +207,8 @@ Here everything works fine; all hooks keep their states. Now consider that we removed `HookB`. We now have: ```dart -context.use(HookA()); -context.use(HookC()); +Hook.use(HookA()); +Hook.use(HookC()); ``` In this situation, `HookA` keeps its state but `HookC` gets a hard reset. @@ -225,9 +225,9 @@ Functions is by far the most common way to write a hook. Thanks to hooks being c The following defines a custom hook that creates a variable and logs its value on the console whenever the value changes: ```dart -ValueNotifier useLoggedState(HookContext context, [T initialData]) { - final result = context.useState(initialData); - context.useValueChanged(result.value, (_, __) { +ValueNotifier useLoggedState(BuildContext context, [T initialData]) { + final result = useState(initialData); + useValueChanged(result.value, (_, __) { print(result.value); }); return result; @@ -236,11 +236,11 @@ ValueNotifier useLoggedState(HookContext context, [T initialData]) { - A class -When a hook becomes too complex, it is possible to convert it into a class that extends `Hook`, which can then be used using `HookContext.use`. As a class, the hook will look very similar to a `State` and have access to life-cycles and methods such as `initHook`, `dispose` and `setState`. It is usually a good practice to hide the class under a function as such: +When a hook becomes too complex, it is possible to convert it into a class that extends `Hook`, which can then be used using `Hook.use`. As a class, the hook will look very similar to a `State` and have access to life-cycles and methods such as `initHook`, `dispose` and `setState`. It is usually a good practice to hide the class under a function as such: ```dart -Result useMyHook(HookContext context) { - return context.use(_MyHook()); +Result useMyHook(BuildContext context) { + return Hook.use(_MyHook()); } ``` @@ -264,7 +264,7 @@ class _TimeAliveState extends HookState> { } @override - void build(HookContext context) { + void build(BuildContext context) { // this hook doesn't create anything nor uses other hooks } @@ -279,7 +279,7 @@ class _TimeAliveState extends HookState> { ## Existing hooks -`HookContext` comes with a list of predefined hooks that are commonly used. They can be used directly on the `HookContext` instance. The existing hooks are: +Flutter_hooks comes with a list of reusable hooks already provided. They are static methods free to use that includes: - useEffect @@ -291,7 +291,7 @@ The following call to `useEffect` subscribes to a `Stream` and cancel the subscr ```dart Stream stream; -context.useEffect(() { +useEffect(() { final subscribtion = stream.listen(print); // This will cancel the subscribtion when the widget is disposed // or if the callback is called again. @@ -311,8 +311,8 @@ The following code uses `useState` to make a counter application: ```dart class Counter extends HookWidget { @override - Widget build(HookContext context) { - final counter = context.useState(0); + Widget build(BuildContext context) { + final counter = useState(0); return GestureDetector( // automatically triggers a rebuild of Counter widget @@ -333,7 +333,7 @@ The following sample make an http call and return the created `Future`. And if ` ```dart String userId; -final Future response = context.useMemoized(() { +final Future response = useMemoized(() { return http.get('someUrl/$userId'); }, [userId]); ``` @@ -348,7 +348,7 @@ The following example implicitly starts a tween animation whenever `color` chang AnimationController controller; Color color; -final colorTween = context.useValueChanged( +final colorTween = useValueChanged( color, (Color oldColor, Animation oldAnimation) { return ColorTween( @@ -368,7 +368,7 @@ They are the equivalent of both `initState`, `dispose` and `didUpdateWidget` for ```dart Duration duration; -AnimationController controller = context.useAnimationController( +AnimationController controller = useAnimationController( // duration is automatically updates when the widget is rebuilt with a different `duration` duration: duration, ); @@ -381,5 +381,5 @@ A set of hooks that subscribes to an object and calls `setState` accordingly. ```dart Stream stream; // automatically rebuild the widget when a new value is pushed to the stream -AsyncSnapshot snapshot = context.useStream(stream); +AsyncSnapshot snapshot = useStream(stream); ``` diff --git a/example/lib/main.dart b/example/lib/main.dart index e35a79a5..2a40d539 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -21,7 +21,7 @@ class _Counter extends HookWidget { const _Counter({Key key}) : super(key: key); @override - Widget build(HookContext context) { + Widget build(BuildContext context) { StreamController countController = _useLocalStorageInt(context, 'counter'); return Scaffold( @@ -31,8 +31,7 @@ class _Counter extends HookWidget { body: Center( child: HookBuilder( builder: (context) { - AsyncSnapshot count = - context.useStream(countController.stream); + AsyncSnapshot count = useStream(countController.stream); return !count.hasData // Currently loading value from local storage, or there's an error @@ -49,33 +48,32 @@ class _Counter extends HookWidget { } StreamController _useLocalStorageInt( - HookContext context, + BuildContext context, String key, { int defaultValue = 0, }) { - final controller = context.useStreamController(keys: [key]); + final controller = useStreamController(keys: [key]); - context - // We define a callback that will be called on first build - // and whenever the controller/key change - ..useEffect(() { - // We listen to the data and push new values to local storage - final sub = controller.stream.listen((data) async { - final prefs = await SharedPreferences.getInstance(); - await prefs.setInt(key, data); - }); - // Unsubscribe when the widget is disposed - // or on controller/key change - return sub.cancel; - }, [controller, key]) - // We load the initial value - ..useEffect(() { - SharedPreferences.getInstance().then((prefs) async { - int valueFromStorage = prefs.getInt(key); - controller.add(valueFromStorage ?? defaultValue); - }).catchError(controller.addError); - // ensure the callback is called only on first build - }, [controller, key]); + // We define a callback that will be called on first build + // and whenever the controller/key change + useEffect(() { + // We listen to the data and push new values to local storage + final sub = controller.stream.listen((data) async { + final prefs = await SharedPreferences.getInstance(); + await prefs.setInt(key, data); + }); + // Unsubscribe when the widget is disposed + // or on controller/key change + return sub.cancel; + }, [controller, key]); + // We load the initial value + useEffect(() { + SharedPreferences.getInstance().then((prefs) async { + int valueFromStorage = prefs.getInt(key); + controller.add(valueFromStorage ?? defaultValue); + }).catchError(controller.addError); + // ensure the callback is called only on first build + }, [controller, key]); return controller; } diff --git a/lib/src/hook_impl.dart b/lib/src/hook_impl.dart index 8c07ebae..9e59a3fe 100644 --- a/lib/src/hook_impl.dart +++ b/lib/src/hook_impl.dart @@ -22,7 +22,7 @@ class _MemoizedHookState extends HookState> { } @override - T build(HookContext context) { + T build(BuildContext context) { return value; } } @@ -51,7 +51,7 @@ class _ValueChangedHookState } @override - R build(HookContext context) { + R build(BuildContext context) { return _result; } } @@ -81,7 +81,7 @@ class _StateHookState extends HookState, _StateHook> { } @override - ValueNotifier build(HookContext context) { + ValueNotifier build(BuildContext context) { return _state; } @@ -131,7 +131,7 @@ class _TickerProviderHookState } @override - TickerProvider build(HookContext context) { + TickerProvider build(BuildContext context) { if (_ticker != null) _ticker.muted = !TickerMode.of(context); return this; } @@ -182,9 +182,8 @@ Switching between controller and uncontrolled vsync is not allowed. } @override - AnimationController build(HookContext context) { - final vsync = - hook.vsync ?? context.useSingleTickerProvider(keys: hook.keys); + AnimationController build(BuildContext context) { + final vsync = hook.vsync ?? useSingleTickerProvider(keys: hook.keys); _animationController ??= AnimationController( vsync: vsync, @@ -232,7 +231,7 @@ class _ListenableStateHook extends HookState { } @override - void build(HookContext context) {} + void build(BuildContext context) {} void _listener() { setState(() {}); @@ -314,7 +313,7 @@ class _FutureStateHook extends HookState, _FutureHook> { } @override - AsyncSnapshot build(HookContext context) { + AsyncSnapshot build(BuildContext context) { return _snapshot; } } @@ -386,7 +385,7 @@ class _StreamHookState extends HookState, _StreamHook> { } @override - AsyncSnapshot build(HookContext context) { + AsyncSnapshot build(BuildContext context) { return _summary; } @@ -444,7 +443,7 @@ class _EffectHookState extends HookState { } @override - void build(HookContext context) {} + void build(BuildContext context) {} @override void dispose() { @@ -499,7 +498,7 @@ class _StreamControllerHookState } @override - StreamController build(HookContext context) { + StreamController build(BuildContext context) { return _controller; } diff --git a/lib/src/hook_widget.dart b/lib/src/hook_widget.dart index 4e26de5d..0966704c 100644 --- a/lib/src/hook_widget.dart +++ b/lib/src/hook_widget.dart @@ -12,8 +12,8 @@ part of 'hook.dart'; /// ``` /// class Good extends HookWidget { /// @override -/// Widget build(HookContext context) { -/// final name = context.useState(""); +/// Widget build(BuildContext context) { +/// final name = useState(""); /// // ... /// } /// } @@ -23,9 +23,9 @@ part of 'hook.dart'; /// ``` /// class Bad extends HookWidget { /// @override -/// Widget build(HookContext context) { +/// Widget build(BuildContext context) { /// if (condition) { -/// final name = context.useState(""); +/// final name = useState(""); /// // ... /// } /// } @@ -96,9 +96,8 @@ part of 'hook.dart'; /// ``` /// class Usual extends HookWidget { /// @override -/// Widget build(HookContext context) { -/// final animationController = -/// context.useAnimationController(duration: const Duration(seconds: 1)); +/// Widget build(BuildContext context) { +/// final animationController = useAnimationController(duration: const Duration(seconds: 1)); /// return Container(); /// } /// } @@ -114,6 +113,19 @@ abstract class Hook { /// Allows subclasses to have a `const` constructor const Hook({this.keys}); + /// Register a [Hook] and returns its value + /// + /// [use] must be called withing [HookWidget.build] and + /// all calls to [use] must be made unconditionally, always + /// on the same order. + /// + /// See [Hook] for more explanations. + static R use(Hook hook) { + assert(HookElement._currentContext != null, + '`Hook.use` can only be called from the build method of HookWidget'); + return HookElement._currentContext._use(hook); + } + /// A list of objects that specify if a [HookState] should be reused or a new one should be created. /// /// When a new [Hook] is created, the framework checks if keys matches using [Hook.shouldPreserveState]. @@ -192,7 +204,7 @@ abstract class HookState> { /// /// [build] is where an [HookState] may use other hooks. This restriction is made to ensure that hooks are unconditionally always requested @protected - R build(HookContext context); + R build(BuildContext context); /// Equivalent of [State.didUpdateWidget] for [HookState] @protected @@ -208,7 +220,10 @@ abstract class HookState> { } /// An [Element] that uses a [HookWidget] as its configuration. -class HookElement extends StatefulElement implements HookContext { +class HookElement extends StatefulElement { + /// Creates an element that uses the given widget as its configuration. + HookElement(HookWidget widget) : super(widget); + Iterator _currentHook; int _hookIndex; List _hooks; @@ -218,8 +233,7 @@ class HookElement extends StatefulElement implements HookContext { bool _isFirstBuild; bool _debugShouldDispose; - /// Creates an element that uses the given widget as its configuration. - HookElement(HookWidget widget) : super(widget); + static HookElement _currentContext; @override HookWidget get widget => super.widget as HookWidget; @@ -237,15 +251,15 @@ class HookElement extends StatefulElement implements HookContext { _debugIsBuilding = true; return true; }()); + HookElement._currentContext = this; super.performRebuild(); + HookElement._currentContext = null; // dispose removed items assert(() { - if (_didReassemble) { - while (_currentHook?.current != null) { - _currentHook.current.dispose(); - _currentHook.moveNext(); - _hookIndex++; + if (_didReassemble && _hooks != null) { + for (var i = _hookIndex; i < _hooks.length;) { + _hooks.removeAt(i).dispose(); } } return true; @@ -253,7 +267,7 @@ class HookElement extends StatefulElement implements HookContext { assert(_hookIndex == (_hooks?.length ?? 0), ''' Build for $widget finished with less hooks used than a previous build. Used $_hookIndex hooks while a previous build had ${_hooks.length}. -This may happen if the call to `use` is made under some condition. +This may happen if the call to `Hook.use` is made under some condition. '''); assert(() { @@ -283,8 +297,7 @@ This may happen if the call to `use` is made under some condition. } } - @override - R use(Hook hook) { + R _use(Hook hook) { assert(_debugIsBuilding == true, ''' Hooks should only be called within the build method of a widget. Calling them outside of build method leads to an unstable state and is therefore prohibited @@ -370,100 +383,164 @@ This may happen if the call to `use` is made under some condition. .._hook = hook ..initHook(); } +} - @override - R useValueChanged(T value, R valueChange(T oldValue, R oldResult)) { - return use(_ValueChangedHook(value, valueChange)); - } +/// Watches a value. +/// +/// Whenever [useValueChanged] is called with a diffent [value], calls [valueChange]. +/// The value returned by [useValueChanged] is the latest returned value of [valueChange] or `null`. +R useValueChanged(T value, R valueChange(T oldValue, R oldResult)) { + return Hook.use(_ValueChangedHook(value, valueChange)); +} - @override - AnimationController useAnimationController({ - Duration duration, - String debugLabel, - double initialValue = 0, - double lowerBound = 0, - double upperBound = 1, - TickerProvider vsync, - AnimationBehavior animationBehavior = AnimationBehavior.normal, - List keys, - }) { - return use(_AnimationControllerHook( - duration: duration, - debugLabel: debugLabel, - initialValue: initialValue, - lowerBound: lowerBound, - upperBound: upperBound, - vsync: vsync, - animationBehavior: animationBehavior, - keys: keys, - )); - } +/// Creates an [AnimationController] automatically disposed. +/// +/// If no [vsync] is provided, the [TickerProvider] is implicitly obtained using [useSingleTickerProvider]. +/// If a [vsync] is specified, changing the instance of [vsync] will result in a call to [AnimationController.resync]. +/// It is not possible to switch between implicit and explicit [vsync]. +/// +/// Changing the [duration] parameter automatically updates [AnimationController.duration]. +/// +/// [initialValue], [lowerBound], [upperBound] and [debugLabel] are ignored after the first call. +/// +/// See also: +/// * [AnimationController] +/// * [useAnimation] +AnimationController useAnimationController({ + Duration duration, + String debugLabel, + double initialValue = 0, + double lowerBound = 0, + double upperBound = 1, + TickerProvider vsync, + AnimationBehavior animationBehavior = AnimationBehavior.normal, + List keys, +}) { + return Hook.use(_AnimationControllerHook( + duration: duration, + debugLabel: debugLabel, + initialValue: initialValue, + lowerBound: lowerBound, + upperBound: upperBound, + vsync: vsync, + animationBehavior: animationBehavior, + keys: keys, + )); +} - @override - void useListenable(Listenable listenable) { - use(_ListenableHook(listenable)); - } +/// Subscribes to a [Listenable] and mark the widget as needing build +/// whenever the listener is called. +/// +/// See also: +/// * [Listenable] +/// * [useValueListenable], [useAnimation], [useStream] +void useListenable(Listenable listenable) { + Hook.use(_ListenableHook(listenable)); +} - @override - T useAnimation(Animation animation) { - useListenable(animation); - return animation.value; - } +/// Subscribes to an [Animation] and return its value. +/// +/// See also: +/// * [Animation] +/// * [useValueListenable], [useListenable], [useStream] +T useAnimation(Animation animation) { + useListenable(animation); + return animation.value; +} - @override - T useValueListenable(ValueListenable valueListenable) { - useListenable(valueListenable); - return valueListenable.value; - } +/// Subscribes to a [ValueListenable] and return its value. +/// +/// See also: +/// * [ValueListenable] +/// * [useListenable], [useAnimation], [useStream] +T useValueListenable(ValueListenable valueListenable) { + useListenable(valueListenable); + return valueListenable.value; +} - @override - AsyncSnapshot useStream(Stream stream, {T initialData}) { - return use(_StreamHook(stream, initialData: initialData)); - } +/// Subscribes to a [Stream] and return its current state in an [AsyncSnapshot]. +/// +/// See also: +/// * [Stream] +/// * [useValueListenable], [useListenable], [useAnimation] +AsyncSnapshot useStream(Stream stream, {T initialData}) { + return Hook.use(_StreamHook(stream, initialData: initialData)); +} - @override - AsyncSnapshot useFuture(Future future, {T initialData}) { - return use(_FutureHook(future, initialData: initialData)); - } +/// Subscribes to a [Future] and return its current state in an [AsyncSnapshot]. +/// +/// See also: +/// * [Future] +/// * [useValueListenable], [useListenable], [useAnimation] +AsyncSnapshot useFuture(Future future, {T initialData}) { + return Hook.use(_FutureHook(future, initialData: initialData)); +} - @override - TickerProvider useSingleTickerProvider({List keys}) { - return use( - keys != null ? _TickerProviderHook(keys) : const _TickerProviderHook(), - ); - } +/// Creates a single usage [TickerProvider]. +/// +/// See also: +/// * [SingleTickerProviderStateMixin] +TickerProvider useSingleTickerProvider({List keys}) { + return Hook.use( + keys != null ? _TickerProviderHook(keys) : const _TickerProviderHook(), + ); +} - @override - void useEffect(VoidCallback Function() effect, [List keys]) { - use(_EffectHook(effect, keys)); - } +/// A hook for side-effects +/// +/// [useEffect] is called synchronously on every [HookWidget.build], unless +/// [keys] is specified. In which case [useEffect] is called again only if +/// any value inside [keys] as changed. +void useEffect(VoidCallback Function() effect, [List keys]) { + Hook.use(_EffectHook(effect, keys)); +} - @override - T useMemoized(T Function() valueBuilder, [List keys = const []]) { - return use(_MemoizedHook( - valueBuilder, - keys: keys, - )); - } +/// Create and cache the instance of an object. +/// +/// [useMemoized] will immediatly call [valueBuilder] on first call and store its result. +/// Later calls to [useMemoized] will reuse the created instance. +/// +/// * [keys] can be use to specify a list of objects for [useMemoized] to watch. +/// So that whenever [Object.operator==] fails on any parameter or if the length of [keys] changes, +/// [valueBuilder] is called again. +T useMemoized(T Function() valueBuilder, [List keys = const []]) { + return Hook.use(_MemoizedHook( + valueBuilder, + keys: keys, + )); +} - @override - ValueNotifier useState([T initialData]) { - return use(_StateHook(initialData: initialData)); - } +/// Create value and subscribes to it. +/// +/// Whenever [ValueNotifier.value] updates, it will mark the caller [HookWidget] +/// as needing build. +/// On first call, inits [ValueNotifier] to [initialData]. [initialData] is ignored +/// on subsequent calls. +/// +/// See also: +/// +/// * [ValueNotifier] +/// * [useStreamController], an alternative to [ValueNotifier] for state. +ValueNotifier useState([T initialData]) { + return Hook.use(_StateHook(initialData: initialData)); +} - @override - StreamController useStreamController( - {bool sync = false, - VoidCallback onListen, - VoidCallback onCancel, - List keys}) { - return use(_StreamControllerHook( - onCancel: onCancel, - onListen: onListen, - sync: sync, - keys: keys, - )); - } +/// Creates a [StreamController] automatically disposed. +/// +/// See also: +/// * [StreamController] +/// * [useStream] +StreamController useStreamController( + {bool sync = false, + VoidCallback onListen, + VoidCallback onCancel, + List keys}) { + return Hook.use(_StreamControllerHook( + onCancel: onCancel, + onListen: onListen, + sync: sync, + keys: keys, + )); } /// A [Widget] that can use [Hook] @@ -490,7 +567,7 @@ abstract class HookWidget extends StatefulWidget { /// /// * [StatelessWidget.build] @protected - Widget build(covariant HookContext context); + Widget build(BuildContext context); } class _HookWidgetState extends State { @@ -504,7 +581,7 @@ class _HookWidgetState extends State { } @override - Widget build(covariant HookContext context) { + Widget build(BuildContext context) { return widget.build(context); } } @@ -513,9 +590,9 @@ class _HookWidgetState extends State { class HookBuilder extends HookWidget { /// The callback used by [HookBuilder] to create a widget. /// - /// If the passed [HookContext] trigger a rebuild, [builder] will be called again. + /// If a [Hook] asks for a rebuild, [builder] will be called again. /// [builder] must not return `null`. - final Widget Function(HookContext context) builder; + final Widget Function(BuildContext context) builder; /// Creates a widget that delegates its build to a callback. /// @@ -527,135 +604,5 @@ class HookBuilder extends HookWidget { super(key: key); @override - Widget build(HookContext context) => builder(context); -} - -/// A [BuildContext] that can use a [Hook]. -/// -/// See also: -/// -/// * [BuildContext] -abstract class HookContext extends BuildContext { - /// Register a [Hook] and returns its value - /// - /// [use] must be called withing [HookWidget.build] and - /// all calls to [use] must be made unconditionally, always - /// on the same order. - /// - /// See [Hook] for more explanations. - R use(Hook hook); - - /// A hook for side-effects - /// - /// [useEffect] is called synchronously on every [HookWidget.build], unless - /// [parameters] is specified. In which case [useEffect] is called again only if - /// any value inside [parameters] as changed. - void useEffect(VoidCallback effect(), [List parameters]); - - /// Create value and subscribes to it. - /// - /// Whenever [ValueNotifier.value] updates, it will mark the caller [HookContext] - /// as needing build. - /// On first call, inits [ValueNotifier] to [initialData]. [initialData] is ignored - /// on subsequent calls. - /// - /// See also: - /// - /// * [ValueNotifier] - /// * [useStreamController], an alternative to [ValueNotifier] for state. - ValueNotifier useState([T initialData]); - - /// Create and cache the instance of an object. - /// - /// [useMemoized] will immediatly call [valueBuilder] on first call and store its result. - /// Later calls to [useMemoized] will reuse the created instance. - /// - /// * [keys] can be use to specify a list of objects for [useMemoized] to watch. - /// So that whenever [operator==] fails on any parameter or if the length of [keys] changes, - /// [valueBuilder] is called again. - T useMemoized(T valueBuilder(), [List keys = const []]); - - /// Watches a value. - /// - /// Whenever [useValueChanged] is called with a diffent [value], calls [valueChange]. - /// The value returned by [useValueChanged] is the latest returned value of [valueChange] or `null`. - R useValueChanged(T value, R valueChange(T oldValue, R oldResult)); - - /// Creates a single usage [TickerProvider]. - /// - /// See also: - /// * [SingleTickerProviderStateMixin] - TickerProvider useSingleTickerProvider({List keys}); - - /// Creates an [AnimationController] automatically disposed. - /// - /// If no [vsync] is provided, the [TickerProvider] is implicitly obtained using [useSingleTickerProvider]. - /// If a [vsync] is specified, changing the instance of [vsync] will result in a call to [AnimationController.resync]. - /// It is not possible to switch between implicit and explicit [vsync]. - /// - /// Changing the [duration] parameter automatically updates [AnimationController.duration]. - /// - /// [initialValue], [lowerBound], [upperBound] and [debugLabel] are ignored after the first call. - /// - /// See also: - /// * [AnimationController] - /// * [HookContext.useAnimation] - AnimationController useAnimationController({ - Duration duration, - String debugLabel, - double initialValue = 0, - double lowerBound = 0, - double upperBound = 1, - TickerProvider vsync, - AnimationBehavior animationBehavior = AnimationBehavior.normal, - List keys, - }); - - /// Creates a [StreamController] automatically disposed. - /// - /// See also: - /// * [StreamController] - /// * [HookContext.useStream] - StreamController useStreamController({ - bool sync = false, - VoidCallback onListen, - VoidCallback onCancel, - List keys, - }); - - /// Subscribes to a [Listenable] and mark the widget as needing build - /// whenever the listener is called. - /// - /// See also: - /// * [Listenable] - /// * [HookContext.useValueListenable], [HookContext.useAnimation], [HookContext.useStream] - void useListenable(Listenable listenable); - - /// Subscribes to a [ValueListenable] and return its value. - /// - /// See also: - /// * [ValueListenable] - /// * [HookContext.useListenable], [HookContext.useAnimation], [HookContext.useStream] - T useValueListenable(ValueListenable valueListenable); - - /// Subscribes to an [Animation] and return its value. - /// - /// See also: - /// * [Animation] - /// * [HookContext.useValueListenable], [HookContext.useListenable], [HookContext.useStream] - T useAnimation(Animation animation); - - /// Subscribes to a [Stream] and return its current state in an [AsyncSnapshot]. - /// - /// See also: - /// * [Stream] - /// * [HookContext.useValueListenable], [HookContext.useListenable], [HookContext.useAnimation] - AsyncSnapshot useStream(Stream stream, {T initialData}); - - /// Subscribes to a [Future] and return its current state in an [AsyncSnapshot]. - /// - /// See also: - /// * [Future] - /// * [HookContext.useValueListenable], [HookContext.useListenable], [HookContext.useAnimation] - AsyncSnapshot useFuture(Future future, {T initialData}); + Widget build(BuildContext context) => builder(context); } diff --git a/test/hook_builder_test.dart b/test/hook_builder_test.dart index 16608c56..558f54f7 100644 --- a/test/hook_builder_test.dart +++ b/test/hook_builder_test.dart @@ -6,7 +6,7 @@ import 'mock.dart'; void main() { testWidgets('simple build', (tester) async { - final fn = Func1(); + final fn = Func1(); when(fn.call(any)).thenAnswer((_) { return Container(); }); diff --git a/test/hook_widget_test.dart b/test/hook_widget_test.dart index 34407278..519a5b9e 100644 --- a/test/hook_widget_test.dart +++ b/test/hook_widget_test.dart @@ -6,11 +6,11 @@ import 'package:flutter_hooks/flutter_hooks.dart'; import 'mock.dart'; void main() { - final build = Func1(); + final build = Func1(); final dispose = Func0(); final initHook = Func0(); final didUpdateHook = Func1(); - final builder = Func1(); + final builder = Func1(); final createHook = () => HookTest( build: build.call, @@ -33,9 +33,8 @@ void main() { final dispose2 = Func0(); when(builder.call(any)).thenAnswer((invocation) { - (invocation.positionalArguments[0] as HookContext) - ..use(HookTest(dispose: dispose.call, keys: keys)) - ..use(HookTest(dispose: dispose2.call, keys: keys2)); + Hook.use(HookTest(dispose: dispose.call, keys: keys)); + Hook.use(HookTest(dispose: dispose2.call, keys: keys2)); return Container(); }); await tester.pumpWidget(HookBuilder(builder: builder.call)); @@ -62,15 +61,14 @@ void main() { when(createState.call()).thenReturn(HookStateTest()); when(builder.call(any)).thenAnswer((invocation) { - (invocation.positionalArguments[0] as HookContext) - ..use(HookTest( - build: build.call, - dispose: dispose.call, - didUpdateHook: didUpdateHook.call, - initHook: initHook.call, - keys: keys, - createStateFn: createState.call, - )); + Hook.use(HookTest( + build: build.call, + dispose: dispose.call, + didUpdateHook: didUpdateHook.call, + initHook: initHook.call, + keys: keys, + createStateFn: createState.call, + )); return Container(); }); await tester.pumpWidget(HookBuilder(builder: builder.call)); @@ -173,7 +171,7 @@ void main() { await tester.pumpWidget(HookBuilder( builder: (context) { hookContext = context as HookElement; - state = context.use(hook); + state = Hook.use(hook); return Container(); }, )); @@ -190,13 +188,12 @@ void main() { testWidgets('life-cycles in order', (tester) async { int result; - HookTest previousHook; + HookTest hook; when(build.call(any)).thenReturn(42); when(builder.call(any)).thenAnswer((invocation) { - HookContext context = invocation.positionalArguments[0]; - previousHook = createHook(); - result = context.use(previousHook); + hook = createHook(); + result = Hook.use(hook); return Container(); }); @@ -204,42 +201,45 @@ void main() { builder: builder.call, )); + final context = tester.firstElement(find.byType(HookBuilder)); expect(result, 42); verifyInOrder([ initHook.call(), - build.call(any), + build.call(context), ]); verifyZeroInteractions(didUpdateHook); verifyZeroInteractions(dispose); - when(build.call(any)).thenReturn(24); + when(build.call(context)).thenReturn(24); + var previousHook = hook; + await tester.pumpWidget(HookBuilder( builder: builder.call, )); expect(result, 24); verifyInOrder([ - // ignore: todo - // TODO: previousHook instead of any - didUpdateHook.call(any), + didUpdateHook.call(previousHook), build.call(any), ]); - verifyNever(initHook.call()); + verifyNoMoreInteractions(initHook); verifyZeroInteractions(dispose); + previousHook = hook; await tester.pump(); - verifyNever(initHook.call()); - verifyNever(didUpdateHook.call(any)); - verifyNever(build.call(any)); + verifyNoMoreInteractions(initHook); + verifyNoMoreInteractions(didUpdateHook); + verifyNoMoreInteractions(build); verifyZeroInteractions(dispose); await tester.pumpWidget(const SizedBox()); - verifyNever(initHook.call()); - verifyNever(didUpdateHook.call(any)); - verifyNever(build.call(any)); + verifyNoMoreInteractions(initHook); + verifyNoMoreInteractions(didUpdateHook); + verifyNoMoreInteractions(build); verify(dispose.call()); + verifyNoMoreInteractions(dispose); }); testWidgets('dispose all called even on failed', (tester) async { @@ -247,9 +247,8 @@ void main() { when(build.call(any)).thenReturn(42); when(builder.call(any)).thenAnswer((invocation) { - invocation.positionalArguments[0] - ..use(createHook()) - ..use(HookTest(dispose: dispose2)); + Hook.use(createHook()); + Hook.use(HookTest(dispose: dispose2)); return Container(); }); @@ -271,7 +270,7 @@ void main() { final hook = createHook(); when(builder.call(any)).thenAnswer((invocation) { - invocation.positionalArguments[0].use(hook); + Hook.use(hook); return Container(); }); @@ -296,14 +295,14 @@ void main() { testWidgets('rebuild with different hooks crash', (tester) async { when(builder.call(any)).thenAnswer((invocation) { - invocation.positionalArguments[0].use(HookTest()); + Hook.use(HookTest()); return Container(); }); await tester.pumpWidget(HookBuilder(builder: builder.call)); when(builder.call(any)).thenAnswer((invocation) { - invocation.positionalArguments[0].use(HookTest()); + Hook.use(HookTest()); return Container(); }); @@ -314,15 +313,15 @@ void main() { }); testWidgets('rebuild added hooks crash', (tester) async { when(builder.call(any)).thenAnswer((invocation) { - invocation.positionalArguments[0].use(HookTest()); + Hook.use(HookTest()); return Container(); }); await tester.pumpWidget(HookBuilder(builder: builder.call)); when(builder.call(any)).thenAnswer((invocation) { - invocation.positionalArguments[0].use(HookTest()); - invocation.positionalArguments[0].use(HookTest()); + Hook.use(HookTest()); + Hook.use(HookTest()); return Container(); }); @@ -332,7 +331,7 @@ void main() { testWidgets('rebuild removed hooks crash', (tester) async { when(builder.call(any)).thenAnswer((invocation) { - invocation.positionalArguments[0].use(HookTest()); + Hook.use(HookTest()); return Container(); }); @@ -353,10 +352,7 @@ void main() { await tester.pumpWidget(HookBuilder(builder: builder.call)); - final context = - tester.firstElement(find.byType(HookBuilder)) as HookElement; - - expect(() => context.use(HookTest()), throwsAssertionError); + expect(() => Hook.use(HookTest()), throwsAssertionError); }); testWidgets('hot-reload triggers a build', (tester) async { @@ -365,9 +361,8 @@ void main() { when(build.call(any)).thenReturn(42); when(builder.call(any)).thenAnswer((invocation) { - HookContext context = invocation.positionalArguments[0]; previousHook = createHook(); - result = context.use(previousHook); + result = Hook.use(previousHook); return Container(); }); @@ -402,11 +397,10 @@ void main() { final dispose2 = Func0(); final initHook2 = Func0(); final didUpdateHook2 = Func1(); - final build2 = Func1(); + final build2 = Func1(); when(builder.call(any)).thenAnswer((invocation) { - (invocation.positionalArguments[0] as HookContext) - ..use(hook1 = createHook()); + Hook.use(hook1 = createHook()); return Container(); }); @@ -422,14 +416,13 @@ void main() { verifyZeroInteractions(didUpdateHook); when(builder.call(any)).thenAnswer((invocation) { - (invocation.positionalArguments[0] as HookContext) - ..use(createHook()) - ..use(HookTest( - initHook: initHook2, - build: build2, - didUpdateHook: didUpdateHook2, - dispose: dispose2, - )); + Hook.use(createHook()); + Hook.use(HookTest( + initHook: initHook2, + build: build2, + didUpdateHook: didUpdateHook2, + dispose: dispose2, + )); return Container(); }); @@ -453,10 +446,10 @@ void main() { final dispose2 = Func0(); final initHook2 = Func0(); final didUpdateHook2 = Func1(); - final build2 = Func1(); + final build2 = Func1(); when(builder.call(any)).thenAnswer((invocation) { - (invocation.positionalArguments[0] as HookContext)..use(createHook()); + Hook.use(createHook()); return Container(); }); @@ -472,14 +465,13 @@ void main() { verifyZeroInteractions(didUpdateHook); when(builder.call(any)).thenAnswer((invocation) { - (invocation.positionalArguments[0] as HookContext) - ..use(HookTest( - initHook: initHook2, - build: build2, - didUpdateHook: didUpdateHook2, - dispose: dispose2, - )) - ..use(createHook()); + Hook.use(HookTest( + initHook: initHook2, + build: build2, + didUpdateHook: didUpdateHook2, + dispose: dispose2, + )); + Hook.use(createHook()); return Container(); }); @@ -502,17 +494,16 @@ void main() { final dispose2 = Func0(); final initHook2 = Func0(); final didUpdateHook2 = Func1(); - final build2 = Func1(); + final build2 = Func1(); when(builder.call(any)).thenAnswer((invocation) { - (invocation.positionalArguments[0] as HookContext) - ..use(createHook()) - ..use(HookTest( - initHook: initHook2, - build: build2, - didUpdateHook: didUpdateHook2, - dispose: dispose2, - )); + Hook.use(createHook()); + Hook.use(HookTest( + initHook: initHook2, + build: build2, + didUpdateHook: didUpdateHook2, + dispose: dispose2, + )); return Container(); }); @@ -557,24 +548,23 @@ void main() { final dispose2 = Func0(); final initHook2 = Func0(); final didUpdateHook2 = Func1(); - final build2 = Func1(); + final build2 = Func1(); final dispose3 = Func0(); final initHook3 = Func0(); final didUpdateHook3 = Func1(); - final build3 = Func1(); + final build3 = Func1(); final dispose4 = Func0(); final initHook4 = Func0(); final didUpdateHook4 = Func1(); - final build4 = Func1(); + final build4 = Func1(); when(builder.call(any)).thenAnswer((invocation) { - (invocation.positionalArguments[0] as HookContext) - ..use(hook1 = createHook()) - ..use(HookTest(dispose: dispose2)) - ..use(HookTest(dispose: dispose3)) - ..use(HookTest(dispose: dispose4)); + Hook.use(hook1 = createHook()); + Hook.use(HookTest(dispose: dispose2)); + Hook.use(HookTest(dispose: dispose3)); + Hook.use(HookTest(dispose: dispose4)); return Container(); }); @@ -604,24 +594,23 @@ void main() { clearInteractions(build4); when(builder.call(any)).thenAnswer((invocation) { - (invocation.positionalArguments[0] as HookContext) - ..use(createHook()) - // changed type from HookTest - ..use(HookTest( - initHook: initHook2, - build: build2, - didUpdateHook: didUpdateHook2, - )) - ..use(HookTest( - initHook: initHook3, - build: build3, - didUpdateHook: didUpdateHook3, - )) - ..use(HookTest( - initHook: initHook4, - build: build4, - didUpdateHook: didUpdateHook4, - )); + Hook.use(createHook()); + // changed type from HookTest + Hook.use(HookTest( + initHook: initHook2, + build: build2, + didUpdateHook: didUpdateHook2, + )); + Hook.use(HookTest( + initHook: initHook3, + build: build3, + didUpdateHook: didUpdateHook3, + )); + Hook.use(HookTest( + initHook: initHook4, + build: build4, + didUpdateHook: didUpdateHook4, + )); return Container(); }); @@ -654,24 +643,23 @@ void main() { final dispose2 = Func0(); final initHook2 = Func0(); final didUpdateHook2 = Func1(); - final build2 = Func1(); + final build2 = Func1(); final dispose3 = Func0(); final initHook3 = Func0(); final didUpdateHook3 = Func1(); - final build3 = Func1(); + final build3 = Func1(); final dispose4 = Func0(); final initHook4 = Func0(); final didUpdateHook4 = Func1(); - final build4 = Func1(); + final build4 = Func1(); when(builder.call(any)).thenAnswer((invocation) { - (invocation.positionalArguments[0] as HookContext) - ..use(hook1 = createHook()) - ..use(HookTest(dispose: dispose2)) - ..use(HookTest(dispose: dispose3)) - ..use(HookTest(dispose: dispose4)); + Hook.use(hook1 = createHook()); + Hook.use(HookTest(dispose: dispose2)); + Hook.use(HookTest(dispose: dispose3)); + Hook.use(HookTest(dispose: dispose4)); return Container(); }); @@ -701,24 +689,23 @@ void main() { clearInteractions(build4); when(builder.call(any)).thenAnswer((invocation) { - (invocation.positionalArguments[0] as HookContext) - ..use(createHook()) - // changed type from HookTest - ..use(HookTest( - initHook: initHook2, - build: build2, - didUpdateHook: didUpdateHook2, - )) - ..use(HookTest( - initHook: initHook3, - build: build3, - didUpdateHook: didUpdateHook3, - )) - ..use(HookTest( - initHook: initHook4, - build: build4, - didUpdateHook: didUpdateHook4, - )); + Hook.use(createHook()); + // changed type from HookTest + Hook.use(HookTest( + initHook: initHook2, + build: build2, + didUpdateHook: didUpdateHook2, + )); + Hook.use(HookTest( + initHook: initHook3, + build: build3, + didUpdateHook: didUpdateHook3, + )); + Hook.use(HookTest( + initHook: initHook4, + build: build4, + didUpdateHook: didUpdateHook4, + )); return Container(); }); @@ -764,7 +751,7 @@ class MyHook extends Hook { class MyHookState extends HookState { @override - MyHookState build(HookContext context) { + MyHookState build(BuildContext context) { return this; } } diff --git a/test/memoized_test.dart b/test/memoized_test.dart index c137fa12..32026c4e 100644 --- a/test/memoized_test.dart +++ b/test/memoized_test.dart @@ -4,7 +4,7 @@ import 'package:flutter_hooks/flutter_hooks.dart'; import 'mock.dart'; void main() { - final builder = Func1(); + final builder = Func1(); final parameterBuilder = Func0(); final valueBuilder = Func0(); @@ -16,13 +16,13 @@ void main() { testWidgets('invalid parameters', (tester) async { await tester.pumpWidget(HookBuilder(builder: (context) { - context.useMemoized(null); + useMemoized(null); return Container(); })); expect(tester.takeException(), isAssertionError); await tester.pumpWidget(HookBuilder(builder: (context) { - context.useMemoized(() {}, null); + useMemoized(() {}, null); return Container(); })); expect(tester.takeException(), isAssertionError); @@ -35,8 +35,7 @@ void main() { when(valueBuilder.call()).thenReturn(42); when(builder.call(any)).thenAnswer((invocation) { - final HookContext context = invocation.positionalArguments.single; - result = context.useMemoized(valueBuilder.call); + result = useMemoized(valueBuilder.call); return Container(); }); @@ -65,9 +64,7 @@ void main() { when(parameterBuilder.call()).thenReturn([]); when(builder.call(any)).thenAnswer((invocation) { - final HookContext context = invocation.positionalArguments.single; - result = - context.useMemoized(valueBuilder.call, parameterBuilder.call()); + result = useMemoized(valueBuilder.call, parameterBuilder.call()); return Container(); }); @@ -131,9 +128,7 @@ void main() { int result; when(builder.call(any)).thenAnswer((invocation) { - final HookContext context = invocation.positionalArguments.single; - result = - context.useMemoized(valueBuilder.call, parameterBuilder.call()); + result = useMemoized(valueBuilder.call, parameterBuilder.call()); return Container(); }); @@ -209,9 +204,7 @@ void main() { final parameters = []; when(builder.call(any)).thenAnswer((invocation) { - final HookContext context = invocation.positionalArguments.single; - result = - context.useMemoized(valueBuilder.call, parameterBuilder.call()); + result = useMemoized(valueBuilder.call, parameterBuilder.call()); return Container(); }); diff --git a/test/mock.dart b/test/mock.dart index 12c0faa8..2ecdc574 100644 --- a/test/mock.dart +++ b/test/mock.dart @@ -26,7 +26,7 @@ abstract class _Func2 { class Func2 extends Mock implements _Func2 {} class HookTest extends Hook { - final R Function(HookContext context) build; + final R Function(BuildContext context) build; final void Function() dispose; final void Function() initHook; final void Function(HookTest previousHook) didUpdateHook; @@ -72,7 +72,7 @@ class HookStateTest extends HookState> { } @override - R build(HookContext context) { + R build(BuildContext context) { if (hook.build != null) { return hook.build(context); } diff --git a/test/use_animation_controller_test.dart b/test/use_animation_controller_test.dart index 5d31fbee..b3e10777 100644 --- a/test/use_animation_controller_test.dart +++ b/test/use_animation_controller_test.dart @@ -10,7 +10,7 @@ void main() { await tester.pumpWidget( HookBuilder(builder: (context) { - controller = context.useAnimationController(); + controller = useAnimationController(); return Container(); }), ); @@ -43,7 +43,7 @@ void main() { await tester.pumpWidget( HookBuilder(builder: (context) { - controller = context.useAnimationController( + controller = useAnimationController( vsync: provider, animationBehavior: AnimationBehavior.preserve, duration: const Duration(seconds: 1), @@ -77,7 +77,7 @@ void main() { await tester.pumpWidget( HookBuilder(builder: (context) { - controller = context.useAnimationController( + controller = useAnimationController( vsync: provider, animationBehavior: AnimationBehavior.normal, duration: const Duration(seconds: 2), @@ -108,7 +108,7 @@ void main() { (tester) async { await tester.pumpWidget(HookBuilder( builder: (context) { - context.useAnimationController(); + useAnimationController(); return Container(); }, )); @@ -116,7 +116,7 @@ void main() { await expectPump( () => tester.pumpWidget(HookBuilder( builder: (context) { - context.useAnimationController(vsync: tester); + useAnimationController(vsync: tester); return Container(); }, )), @@ -128,7 +128,7 @@ void main() { // the other way around await tester.pumpWidget(HookBuilder( builder: (context) { - context.useAnimationController(vsync: tester); + useAnimationController(vsync: tester); return Container(); }, )); @@ -136,7 +136,7 @@ void main() { await expectPump( () => tester.pumpWidget(HookBuilder( builder: (context) { - context.useAnimationController(); + useAnimationController(); return Container(); }, )), @@ -149,7 +149,7 @@ void main() { AnimationController controller; await tester.pumpWidget(HookBuilder( builder: (context) { - controller = context.useAnimationController(keys: keys); + controller = useAnimationController(keys: keys); return Container(); }, )); @@ -159,7 +159,7 @@ void main() { await tester.pumpWidget(HookBuilder( builder: (context) { - controller = context.useAnimationController(keys: keys); + controller = useAnimationController(keys: keys); return Container(); }, )); diff --git a/test/use_animation_test.dart b/test/use_animation_test.dart index b8caf8b1..c80a0214 100644 --- a/test/use_animation_test.dart +++ b/test/use_animation_test.dart @@ -8,7 +8,7 @@ void main() { await expectPump( () => tester.pumpWidget(HookBuilder( builder: (context) { - context.useAnimation(null); + useAnimation(null); return Container(); }, )), @@ -21,7 +21,7 @@ void main() { pump() => tester.pumpWidget(HookBuilder( builder: (context) { - result = context.useAnimation(listenable); + result = useAnimation(listenable); return Container(); }, )); diff --git a/test/use_effect_test.dart b/test/use_effect_test.dart index 7b00106a..1ebfdccb 100644 --- a/test/use_effect_test.dart +++ b/test/use_effect_test.dart @@ -8,7 +8,7 @@ final unrelated = Func0(); List parameters; Widget builder() => HookBuilder(builder: (context) { - context.useEffect(effect.call, parameters); + useEffect(effect.call, parameters); unrelated.call(); return Container(); }); @@ -22,7 +22,7 @@ void main() { testWidgets('useEffect null callback throws', (tester) async { await expectPump( () => tester.pumpWidget(HookBuilder(builder: (c) { - c.useEffect(null); + useEffect(null); return Container(); })), throwsAssertionError, @@ -36,7 +36,7 @@ void main() { when(effect.call()).thenReturn(dispose.call); builder() => HookBuilder(builder: (context) { - context.useEffect(effect.call); + useEffect(effect.call); unrelated.call(); return Container(); }); @@ -181,7 +181,7 @@ void main() { List parameters; builder() => HookBuilder(builder: (context) { - context.useEffect(effect.call, parameters); + useEffect(effect.call, parameters); return Container(); }); diff --git a/test/use_future_test.dart b/test/use_future_test.dart index f5692a3c..0554e760 100644 --- a/test/use_future_test.dart +++ b/test/use_future_test.dart @@ -6,10 +6,10 @@ import 'package:flutter_hooks/flutter_hooks.dart'; import 'mock.dart'; void main() { - Widget Function(HookContext) snapshotText(Future stream, + Widget Function(BuildContext) snapshotText(Future stream, {String initialData}) { return (context) { - final snapshot = context.useFuture(stream, initialData: initialData); + final snapshot = useFuture(stream, initialData: initialData); return Text(snapshot.toString(), textDirection: TextDirection.ltr); }; } diff --git a/test/use_listenable_test.dart b/test/use_listenable_test.dart index fdb579c2..6ce5caef 100644 --- a/test/use_listenable_test.dart +++ b/test/use_listenable_test.dart @@ -8,7 +8,7 @@ void main() { await expectPump( () => tester.pumpWidget(HookBuilder( builder: (context) { - context.useListenable(null); + useListenable(null); return Container(); }, )), @@ -19,7 +19,7 @@ void main() { pump() => tester.pumpWidget(HookBuilder( builder: (context) { - context.useListenable(listenable); + useListenable(listenable); return Container(); }, )); diff --git a/test/use_state_test.dart b/test/use_state_test.dart index e2ceed29..226285be 100644 --- a/test/use_state_test.dart +++ b/test/use_state_test.dart @@ -11,7 +11,7 @@ void main() { await tester.pumpWidget(HookBuilder( builder: (context) { element = context as HookElement; - state = context.useState(42); + state = useState(42); return Container(); }, )); @@ -45,7 +45,7 @@ void main() { await tester.pumpWidget(HookBuilder( builder: (context) { element = context as HookElement; - state = context.useState(); + state = useState(); return Container(); }, )); diff --git a/test/use_stream_controller_test.dart b/test/use_stream_controller_test.dart index 97124e49..21c06dd4 100644 --- a/test/use_stream_controller_test.dart +++ b/test/use_stream_controller_test.dart @@ -11,13 +11,13 @@ void main() { StreamController controller; await tester.pumpWidget(HookBuilder(builder: (context) { - controller = context.useStreamController(); + controller = useStreamController(); return Container(); })); final previous = controller; await tester.pumpWidget(HookBuilder(builder: (context) { - controller = context.useStreamController(keys: []); + controller = useStreamController(keys: []); return Container(); })); @@ -27,7 +27,7 @@ void main() { StreamController controller; await tester.pumpWidget(HookBuilder(builder: (context) { - controller = context.useStreamController(); + controller = useStreamController(); return Container(); })); @@ -41,7 +41,7 @@ void main() { final onListen = () {}; final onCancel = () {}; await tester.pumpWidget(HookBuilder(builder: (context) { - controller = context.useStreamController( + controller = useStreamController( sync: true, onCancel: onCancel, onListen: onListen, @@ -64,7 +64,7 @@ void main() { StreamController controller; await tester.pumpWidget(HookBuilder(builder: (context) { - controller = context.useStreamController(sync: true); + controller = useStreamController(sync: true); return Container(); })); @@ -78,7 +78,7 @@ void main() { final onListen = () {}; final onCancel = () {}; await tester.pumpWidget(HookBuilder(builder: (context) { - controller = context.useStreamController( + controller = useStreamController( onCancel: onCancel, onListen: onListen, ); diff --git a/test/use_stream_test.dart b/test/use_stream_test.dart index 5e6ea01f..63c46dd9 100644 --- a/test/use_stream_test.dart +++ b/test/use_stream_test.dart @@ -8,10 +8,10 @@ import 'mock.dart'; /// port of [StreamBuilder] /// void main() { - Widget Function(HookContext) snapshotText(Stream stream, + Widget Function(BuildContext) snapshotText(Stream stream, {String initialData}) { return (context) { - final snapshot = context.useStream(stream, initialData: initialData); + final snapshot = useStream(stream, initialData: initialData); return Text(snapshot.toString(), textDirection: TextDirection.ltr); }; } diff --git a/test/use_ticker_provider_test.dart b/test/use_ticker_provider_test.dart index a0ef4b35..4132b942 100644 --- a/test/use_ticker_provider_test.dart +++ b/test/use_ticker_provider_test.dart @@ -11,7 +11,7 @@ void main() { await tester.pumpWidget(TickerMode( enabled: true, child: HookBuilder(builder: (context) { - provider = context.useSingleTickerProvider(); + provider = useSingleTickerProvider(); return Container(); }), )); @@ -29,7 +29,7 @@ void main() { testWidgets('useSingleTickerProvider unused', (tester) async { await tester.pumpWidget(HookBuilder(builder: (context) { - context.useSingleTickerProvider(); + useSingleTickerProvider(); return Container(); })); @@ -42,7 +42,7 @@ void main() { await tester.pumpWidget(TickerMode( enabled: true, child: HookBuilder(builder: (context) { - provider = context.useSingleTickerProvider(); + provider = useSingleTickerProvider(); return Container(); }), )); @@ -66,7 +66,7 @@ void main() { List keys; await tester.pumpWidget(HookBuilder(builder: (context) { - provider = context.useSingleTickerProvider(keys: keys); + provider = useSingleTickerProvider(keys: keys); return Container(); })); @@ -74,7 +74,7 @@ void main() { keys = []; await tester.pumpWidget(HookBuilder(builder: (context) { - provider = context.useSingleTickerProvider(keys: keys); + provider = useSingleTickerProvider(keys: keys); return Container(); })); diff --git a/test/use_value_changed_test.dart b/test/use_value_changed_test.dart index 9fc0d7c5..19a6db93 100644 --- a/test/use_value_changed_test.dart +++ b/test/use_value_changed_test.dart @@ -6,12 +6,12 @@ import 'mock.dart'; void main() { testWidgets('useValueChanged basic', (tester) async { var value = 42; - final useValueChanged = Func2(); + final _useValueChanged = Func2(); String result; pump() => tester.pumpWidget(HookBuilder( builder: (context) { - result = context.useValueChanged(value, useValueChanged.call); + result = useValueChanged(value, _useValueChanged.call); return Container(); }, )); @@ -20,43 +20,43 @@ void main() { final HookElement context = find.byType(HookBuilder).evaluate().first; expect(result, null); - verifyNoMoreInteractions(useValueChanged); + verifyNoMoreInteractions(_useValueChanged); expect(context.dirty, false); await pump(); expect(result, null); - verifyNoMoreInteractions(useValueChanged); + verifyNoMoreInteractions(_useValueChanged); expect(context.dirty, false); value++; - when(useValueChanged.call(any, any)).thenReturn('Hello'); + when(_useValueChanged.call(any, any)).thenReturn('Hello'); await pump(); - verify(useValueChanged.call(42, null)); + verify(_useValueChanged.call(42, null)); expect(result, 'Hello'); - verifyNoMoreInteractions(useValueChanged); + verifyNoMoreInteractions(_useValueChanged); expect(context.dirty, false); await pump(); expect(result, 'Hello'); - verifyNoMoreInteractions(useValueChanged); + verifyNoMoreInteractions(_useValueChanged); expect(context.dirty, false); value++; - when(useValueChanged.call(any, any)).thenReturn('Foo'); + when(_useValueChanged.call(any, any)).thenReturn('Foo'); await pump(); expect(result, 'Foo'); - verify(useValueChanged.call(43, 'Hello')); - verifyNoMoreInteractions(useValueChanged); + verify(_useValueChanged.call(43, 'Hello')); + verifyNoMoreInteractions(_useValueChanged); expect(context.dirty, false); await pump(); expect(result, 'Foo'); - verifyNoMoreInteractions(useValueChanged); + verifyNoMoreInteractions(_useValueChanged); expect(context.dirty, false); // dispose @@ -66,7 +66,7 @@ void main() { testWidgets('valueChanged required', (tester) async { await tester.pumpWidget(HookBuilder( builder: (context) { - context.useValueChanged(42, null); + useValueChanged(42, null); return Container(); }, )); diff --git a/test/use_value_listenable_test.dart b/test/use_value_listenable_test.dart index c721092d..d15011d2 100644 --- a/test/use_value_listenable_test.dart +++ b/test/use_value_listenable_test.dart @@ -8,7 +8,7 @@ void main() { await expectPump( () => tester.pumpWidget(HookBuilder( builder: (context) { - context.useValueListenable(null); + useValueListenable(null); return Container(); }, )), @@ -20,7 +20,7 @@ void main() { pump() => tester.pumpWidget(HookBuilder( builder: (context) { - result = context.useValueListenable(listenable); + result = useValueListenable(listenable); return Container(); }, )); From 754d34b82aabdf360acd89f675541d8c79970d94 Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Thu, 27 Dec 2018 09:06:28 +0100 Subject: [PATCH 036/384] changelog --- CHANGELOG.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 94b0f474..bb69e947 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,19 @@ ## 0.2.0: +- Made all existing hooks as static functions, and removed `HookContext`. The migration is as followed: +```dart +Widget build(HookContext context) { + final state = context.useState(0); +} +``` + +becomes: +```dart +Widget build(BuildContext context) { + final state = useState(0); +} +``` + - Introduced keys for hooks and applied them to hooks where it makes sense. - fixes a bug where hot-reload without using hooks throwed an exception From e596e26ec00bed32c8b60d6e9e31a12bbbd07424 Mon Sep 17 00:00:00 2001 From: Vitus Date: Thu, 27 Dec 2018 18:15:43 +0100 Subject: [PATCH 037/384] Fix typos in README --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 590493fd..00cb82b7 100644 --- a/README.md +++ b/README.md @@ -292,10 +292,10 @@ The following call to `useEffect` subscribes to a `Stream` and cancel the subscr ```dart Stream stream; context.useEffect(() { - final subscribtion = stream.listen(print); - // This will cancel the subscribtion when the widget is disposed + final subscription = stream.listen(print); + // This will cancel the subscription when the widget is disposed // or if the callback is called again. - return subscribtion.cancel; + return subscription.cancel; }, // when the stream change, useEffect will call the callback again. [stream], From ee44fd395b3f5379c8fb4c6a5d705d4e1c44ef37 Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Fri, 28 Dec 2018 10:16:40 +0100 Subject: [PATCH 038/384] refactor folders --- lib/flutter_hooks.dart | 3 +- lib/src/{hook_widget.dart => framework.dart} | 160 +---------------- lib/src/hook.dart | 9 - lib/src/{hook_impl.dart => hooks.dart} | 173 ++++++++++++++++++- test/use_ticker_provider_test.dart | 2 +- 5 files changed, 173 insertions(+), 174 deletions(-) rename lib/src/{hook_widget.dart => framework.dart} (73%) delete mode 100644 lib/src/hook.dart rename lib/src/{hook_impl.dart => hooks.dart} (69%) diff --git a/lib/flutter_hooks.dart b/lib/flutter_hooks.dart index 2e371479..dbbf2167 100644 --- a/lib/flutter_hooks.dart +++ b/lib/flutter_hooks.dart @@ -1 +1,2 @@ -export 'package:flutter_hooks/src/hook.dart'; +export 'package:flutter_hooks/src/framework.dart'; +export 'package:flutter_hooks/src/hooks.dart'; diff --git a/lib/src/hook_widget.dart b/lib/src/framework.dart similarity index 73% rename from lib/src/hook_widget.dart rename to lib/src/framework.dart index 0966704c..3a50ca44 100644 --- a/lib/src/hook_widget.dart +++ b/lib/src/framework.dart @@ -1,4 +1,4 @@ -part of 'hook.dart'; +import 'package:flutter/widgets.dart'; /// [Hook] is similar to a [StatelessWidget], but is not associated /// to an [Element]. @@ -385,164 +385,6 @@ This may happen if the call to `Hook.use` is made under some condition. } } -/// Watches a value. -/// -/// Whenever [useValueChanged] is called with a diffent [value], calls [valueChange]. -/// The value returned by [useValueChanged] is the latest returned value of [valueChange] or `null`. -R useValueChanged(T value, R valueChange(T oldValue, R oldResult)) { - return Hook.use(_ValueChangedHook(value, valueChange)); -} - -/// Creates an [AnimationController] automatically disposed. -/// -/// If no [vsync] is provided, the [TickerProvider] is implicitly obtained using [useSingleTickerProvider]. -/// If a [vsync] is specified, changing the instance of [vsync] will result in a call to [AnimationController.resync]. -/// It is not possible to switch between implicit and explicit [vsync]. -/// -/// Changing the [duration] parameter automatically updates [AnimationController.duration]. -/// -/// [initialValue], [lowerBound], [upperBound] and [debugLabel] are ignored after the first call. -/// -/// See also: -/// * [AnimationController] -/// * [useAnimation] -AnimationController useAnimationController({ - Duration duration, - String debugLabel, - double initialValue = 0, - double lowerBound = 0, - double upperBound = 1, - TickerProvider vsync, - AnimationBehavior animationBehavior = AnimationBehavior.normal, - List keys, -}) { - return Hook.use(_AnimationControllerHook( - duration: duration, - debugLabel: debugLabel, - initialValue: initialValue, - lowerBound: lowerBound, - upperBound: upperBound, - vsync: vsync, - animationBehavior: animationBehavior, - keys: keys, - )); -} - -/// Subscribes to a [Listenable] and mark the widget as needing build -/// whenever the listener is called. -/// -/// See also: -/// * [Listenable] -/// * [useValueListenable], [useAnimation], [useStream] -void useListenable(Listenable listenable) { - Hook.use(_ListenableHook(listenable)); -} - -/// Subscribes to an [Animation] and return its value. -/// -/// See also: -/// * [Animation] -/// * [useValueListenable], [useListenable], [useStream] -T useAnimation(Animation animation) { - useListenable(animation); - return animation.value; -} - -/// Subscribes to a [ValueListenable] and return its value. -/// -/// See also: -/// * [ValueListenable] -/// * [useListenable], [useAnimation], [useStream] -T useValueListenable(ValueListenable valueListenable) { - useListenable(valueListenable); - return valueListenable.value; -} - -/// Subscribes to a [Stream] and return its current state in an [AsyncSnapshot]. -/// -/// See also: -/// * [Stream] -/// * [useValueListenable], [useListenable], [useAnimation] -AsyncSnapshot useStream(Stream stream, {T initialData}) { - return Hook.use(_StreamHook(stream, initialData: initialData)); -} - -/// Subscribes to a [Future] and return its current state in an [AsyncSnapshot]. -/// -/// See also: -/// * [Future] -/// * [useValueListenable], [useListenable], [useAnimation] -AsyncSnapshot useFuture(Future future, {T initialData}) { - return Hook.use(_FutureHook(future, initialData: initialData)); -} - -/// Creates a single usage [TickerProvider]. -/// -/// See also: -/// * [SingleTickerProviderStateMixin] -TickerProvider useSingleTickerProvider({List keys}) { - return Hook.use( - keys != null ? _TickerProviderHook(keys) : const _TickerProviderHook(), - ); -} - -/// A hook for side-effects -/// -/// [useEffect] is called synchronously on every [HookWidget.build], unless -/// [keys] is specified. In which case [useEffect] is called again only if -/// any value inside [keys] as changed. -void useEffect(VoidCallback Function() effect, [List keys]) { - Hook.use(_EffectHook(effect, keys)); -} - -/// Create and cache the instance of an object. -/// -/// [useMemoized] will immediatly call [valueBuilder] on first call and store its result. -/// Later calls to [useMemoized] will reuse the created instance. -/// -/// * [keys] can be use to specify a list of objects for [useMemoized] to watch. -/// So that whenever [Object.operator==] fails on any parameter or if the length of [keys] changes, -/// [valueBuilder] is called again. -T useMemoized(T Function() valueBuilder, [List keys = const []]) { - return Hook.use(_MemoizedHook( - valueBuilder, - keys: keys, - )); -} - -/// Create value and subscribes to it. -/// -/// Whenever [ValueNotifier.value] updates, it will mark the caller [HookWidget] -/// as needing build. -/// On first call, inits [ValueNotifier] to [initialData]. [initialData] is ignored -/// on subsequent calls. -/// -/// See also: -/// -/// * [ValueNotifier] -/// * [useStreamController], an alternative to [ValueNotifier] for state. -ValueNotifier useState([T initialData]) { - return Hook.use(_StateHook(initialData: initialData)); -} - -/// Creates a [StreamController] automatically disposed. -/// -/// See also: -/// * [StreamController] -/// * [useStream] -StreamController useStreamController( - {bool sync = false, - VoidCallback onListen, - VoidCallback onCancel, - List keys}) { - return Hook.use(_StreamControllerHook( - onCancel: onCancel, - onListen: onListen, - sync: sync, - keys: keys, - )); -} - /// A [Widget] that can use [Hook] /// /// It's usage is very similar to [StatelessWidget]. diff --git a/lib/src/hook.dart b/lib/src/hook.dart deleted file mode 100644 index caecfc05..00000000 --- a/lib/src/hook.dart +++ /dev/null @@ -1,9 +0,0 @@ -import 'dart:async'; - -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/scheduler.dart'; -import 'package:flutter/widgets.dart'; - -part 'hook_impl.dart'; -part 'hook_widget.dart'; diff --git a/lib/src/hook_impl.dart b/lib/src/hooks.dart similarity index 69% rename from lib/src/hook_impl.dart rename to lib/src/hooks.dart index 9e59a3fe..67085f8f 100644 --- a/lib/src/hook_impl.dart +++ b/lib/src/hooks.dart @@ -1,4 +1,24 @@ -part of 'hook.dart'; +import 'dart:async'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/scheduler.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_hooks/src/framework.dart'; + +/// Create and cache the instance of an object. +/// +/// [useMemoized] will immediatly call [valueBuilder] on first call and store its result. +/// Later calls to [useMemoized] will reuse the created instance. +/// +/// * [keys] can be use to specify a list of objects for [useMemoized] to watch. +/// So that whenever [Object.operator==] fails on any parameter or if the length of [keys] changes, +/// [valueBuilder] is called again. +T useMemoized(T Function() valueBuilder, [List keys = const []]) { + return Hook.use(_MemoizedHook( + valueBuilder, + keys: keys, + )); +} class _MemoizedHook extends Hook { final T Function() valueBuilder; @@ -27,6 +47,14 @@ class _MemoizedHookState extends HookState> { } } +/// Watches a value. +/// +/// Whenever [useValueChanged] is called with a diffent [value], calls [valueChange]. +/// The value returned by [useValueChanged] is the latest returned value of [valueChange] or `null`. +R useValueChanged(T value, R valueChange(T oldValue, R oldResult)) { + return Hook.use(_ValueChangedHook(value, valueChange)); +} + class _ValueChangedHook extends Hook { final R Function(T oldValue, R oldResult) valueChanged; final T value; @@ -56,6 +84,21 @@ class _ValueChangedHookState } } +/// Create value and subscribes to it. +/// +/// Whenever [ValueNotifier.value] updates, it will mark the caller [HookWidget] +/// as needing build. +/// On first call, inits [ValueNotifier] to [initialData]. [initialData] is ignored +/// on subsequent calls. +/// +/// See also: +/// +/// * [ValueNotifier] +/// * [useStreamController], an alternative to [ValueNotifier] for state. +ValueNotifier useState([T initialData]) { + return Hook.use(_StateHook(initialData: initialData)); +} + class _StateHook extends Hook> { final T initialData; @@ -90,15 +133,27 @@ class _StateHookState extends HookState, _StateHook> { } } -class _TickerProviderHook extends Hook { - const _TickerProviderHook([List keys]) : super(keys: keys); +/// Creates a single usage [TickerProvider]. +/// +/// See also: +/// * [SingleTickerProviderStateMixin] +TickerProvider useSingleTickerProvider({List keys}) { + return Hook.use( + keys != null + ? _SingleTickerProviderHook(keys) + : const _SingleTickerProviderHook(), + ); +} + +class _SingleTickerProviderHook extends Hook { + const _SingleTickerProviderHook([List keys]) : super(keys: keys); @override _TickerProviderHookState createState() => _TickerProviderHookState(); } class _TickerProviderHookState - extends HookState + extends HookState implements TickerProvider { Ticker _ticker; @@ -137,6 +192,41 @@ class _TickerProviderHookState } } +/// Creates an [AnimationController] automatically disposed. +/// +/// If no [vsync] is provided, the [TickerProvider] is implicitly obtained using [useSingleTickerProvider]. +/// If a [vsync] is specified, changing the instance of [vsync] will result in a call to [AnimationController.resync]. +/// It is not possible to switch between implicit and explicit [vsync]. +/// +/// Changing the [duration] parameter automatically updates [AnimationController.duration]. +/// +/// [initialValue], [lowerBound], [upperBound] and [debugLabel] are ignored after the first call. +/// +/// See also: +/// * [AnimationController] +/// * [useAnimation] +AnimationController useAnimationController({ + Duration duration, + String debugLabel, + double initialValue = 0, + double lowerBound = 0, + double upperBound = 1, + TickerProvider vsync, + AnimationBehavior animationBehavior = AnimationBehavior.normal, + List keys, +}) { + return Hook.use(_AnimationControllerHook( + duration: duration, + debugLabel: debugLabel, + initialValue: initialValue, + lowerBound: lowerBound, + upperBound: upperBound, + vsync: vsync, + animationBehavior: animationBehavior, + keys: keys, + )); +} + class _AnimationControllerHook extends Hook { final Duration duration; final String debugLabel; @@ -205,6 +295,36 @@ Switching between controller and uncontrolled vsync is not allowed. } } +/// Subscribes to a [ValueListenable] and return its value. +/// +/// See also: +/// * [ValueListenable] +/// * [useListenable], [useAnimation], [useStream] +T useValueListenable(ValueListenable valueListenable) { + useListenable(valueListenable); + return valueListenable.value; +} + +/// Subscribes to a [Listenable] and mark the widget as needing build +/// whenever the listener is called. +/// +/// See also: +/// * [Listenable] +/// * [useValueListenable], [useAnimation], [useStream] +void useListenable(Listenable listenable) { + Hook.use(_ListenableHook(listenable)); +} + +/// Subscribes to an [Animation] and return its value. +/// +/// See also: +/// * [Animation] +/// * [useValueListenable], [useListenable], [useStream] +T useAnimation(Animation animation) { + useListenable(animation); + return animation.value; +} + class _ListenableHook extends Hook { final Listenable listenable; @@ -244,6 +364,15 @@ class _ListenableStateHook extends HookState { } } +/// Subscribes to a [Future] and return its current state in an [AsyncSnapshot]. +/// +/// See also: +/// * [Future] +/// * [useValueListenable], [useListenable], [useAnimation] +AsyncSnapshot useFuture(Future future, {T initialData}) { + return Hook.use(_FutureHook(future, initialData: initialData)); +} + class _FutureHook extends Hook> { final Future future; final T initialData; @@ -318,6 +447,15 @@ class _FutureStateHook extends HookState, _FutureHook> { } } +/// Subscribes to a [Stream] and return its current state in an [AsyncSnapshot]. +/// +/// See also: +/// * [Stream] +/// * [useValueListenable], [useListenable], [useAnimation] +AsyncSnapshot useStream(Stream stream, {T initialData}) { + return Hook.use(_StreamHook(stream, initialData: initialData)); +} + class _StreamHook extends Hook> { final Stream stream; final T initialData; @@ -410,6 +548,15 @@ class _StreamHookState extends HookState, _StreamHook> { current.inState(ConnectionState.none); } +/// A hook for side-effects +/// +/// [useEffect] is called synchronously on every [HookWidget.build], unless +/// [keys] is specified. In which case [useEffect] is called again only if +/// any value inside [keys] as changed. +void useEffect(VoidCallback Function() effect, [List keys]) { + Hook.use(_EffectHook(effect, keys)); +} + class _EffectHook extends Hook { final VoidCallback Function() effect; @@ -458,6 +605,24 @@ class _EffectHookState extends HookState { } } +/// Creates a [StreamController] automatically disposed. +/// +/// See also: +/// * [StreamController] +/// * [useStream] +StreamController useStreamController( + {bool sync = false, + VoidCallback onListen, + VoidCallback onCancel, + List keys}) { + return Hook.use(_StreamControllerHook( + onCancel: onCancel, + onListen: onListen, + sync: sync, + keys: keys, + )); +} + class _StreamControllerHook extends Hook> { final bool sync; final VoidCallback onListen; diff --git a/test/use_ticker_provider_test.dart b/test/use_ticker_provider_test.dart index 4132b942..ae6fe82f 100644 --- a/test/use_ticker_provider_test.dart +++ b/test/use_ticker_provider_test.dart @@ -1,6 +1,6 @@ import 'package:flutter/scheduler.dart'; import 'package:flutter/widgets.dart'; -import 'package:flutter_hooks/src/hook.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; import 'mock.dart'; From 9b751cdb0af3b888368dc94a6b0d5328b11f4233 Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Fri, 28 Dec 2018 12:27:46 +0100 Subject: [PATCH 039/384] useReducer (#23) --- lib/src/framework.dart | 21 ------ lib/src/hooks.dart | 106 ++++++++++++++++++++++++++++++ test/use_reducer_test.dart | 131 +++++++++++++++++++++++++++++++++++++ 3 files changed, 237 insertions(+), 21 deletions(-) create mode 100644 test/use_reducer_test.dart diff --git a/lib/src/framework.dart b/lib/src/framework.dart index 3a50ca44..5630b6be 100644 --- a/lib/src/framework.dart +++ b/lib/src/framework.dart @@ -427,24 +427,3 @@ class _HookWidgetState extends State { return widget.build(context); } } - -/// A [HookWidget] that defer its [HookWidget.build] to a callback -class HookBuilder extends HookWidget { - /// The callback used by [HookBuilder] to create a widget. - /// - /// If a [Hook] asks for a rebuild, [builder] will be called again. - /// [builder] must not return `null`. - final Widget Function(BuildContext context) builder; - - /// Creates a widget that delegates its build to a callback. - /// - /// The [builder] argument must not be null. - const HookBuilder({ - @required this.builder, - Key key, - }) : assert(builder != null), - super(key: key); - - @override - Widget build(BuildContext context) => builder(context); -} diff --git a/lib/src/hooks.dart b/lib/src/hooks.dart index 67085f8f..71246350 100644 --- a/lib/src/hooks.dart +++ b/lib/src/hooks.dart @@ -5,6 +5,112 @@ import 'package:flutter/scheduler.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_hooks/src/framework.dart'; +/// A [HookWidget] that defer its [HookWidget.build] to a callback +class HookBuilder extends HookWidget { + /// The callback used by [HookBuilder] to create a widget. + /// + /// If a [Hook] asks for a rebuild, [builder] will be called again. + /// [builder] must not return `null`. + final Widget Function(BuildContext context) builder; + + /// Creates a widget that delegates its build to a callback. + /// + /// The [builder] argument must not be null. + const HookBuilder({ + @required this.builder, + Key key, + }) : assert(builder != null), + super(key: key); + + @override + Widget build(BuildContext context) => builder(context); +} + +/// A state holder that allows mutations by dispatching actions. +abstract class Store { + /// The current state. + /// + /// This value may change after a call to [dispatch]. + State get state; + + /// Dispatches an action. + /// + /// Actions are dispatched synchronously. + /// It is impossible to try to dispatch actions during [HookWidget.build]. + void dispatch(Action action); +} + +/// Composes an [Action] and a [State] to create a new [State]. +/// +/// [Reducer] must never return `null`, even if [state] or [action] are `null`. +typedef Reducer = State Function(State state, Action action); + +/// An alternative to [useState] for more complex states. +/// +/// [useReducer] manages an read only state that can be updated +/// by dispatching actions which are interpreted by a [Reducer]. +/// +/// [reducer] is immediatly called on first build with [initialAction] +/// and [initialState] as parameter. +/// +/// It is possible to change the [reducer] by calling [useReducer] +/// with a new [Reducer]. +/// +/// See also: +/// * [Reducer] +/// * [Store] +Store useReducer( + Reducer reducer, { + State initialState, + Action initialAction, +}) { + return Hook.use(_ReducerdHook(reducer, + initialAction: initialAction, initialState: initialState)); +} + +class _ReducerdHook extends Hook> { + final Reducer reducer; + final State initialState; + final Action initialAction; + + const _ReducerdHook(this.reducer, {this.initialState, this.initialAction}) + : assert(reducer != null); + + @override + _ReducerdHookState createState() => + _ReducerdHookState(); +} + +class _ReducerdHookState + extends HookState, _ReducerdHook> + implements Store { + @override + State state; + + @override + void initHook() { + super.initHook(); + state = hook.reducer(hook.initialState, hook.initialAction); + assert(state != null); + } + + @override + void dispatch(Action action) { + final res = hook.reducer(state, action); + assert(res != null); + if (state != res) { + setState(() { + state = res; + }); + } + } + + @override + Store build(BuildContext context) { + return this; + } +} + /// Create and cache the instance of an object. /// /// [useMemoized] will immediatly call [valueBuilder] on first call and store its result. diff --git a/test/use_reducer_test.dart b/test/use_reducer_test.dart new file mode 100644 index 00000000..6bf88069 --- /dev/null +++ b/test/use_reducer_test.dart @@ -0,0 +1,131 @@ +import 'package:flutter/widgets.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; + +import 'mock.dart'; + +void main() { + group('useReducer', () { + testWidgets('basic', (tester) async { + final reducer = Func2(); + + Store store; + pump() => tester.pumpWidget(HookBuilder( + builder: (context) { + store = useReducer(reducer.call); + return Container(); + }, + )); + + when(reducer.call(null, null)).thenReturn(0); + await pump(); + final element = tester.firstElement(find.byType(HookBuilder)); + + verify(reducer.call(null, null)).called(1); + verifyNoMoreInteractions(reducer); + + expect(store.state, 0); + + await pump(); + verifyNoMoreInteractions(reducer); + expect(store.state, 0); + + when(reducer.call(0, 'foo')).thenReturn(1); + + store.dispatch('foo'); + + verify(reducer.call(0, 'foo')).called(1); + verifyNoMoreInteractions(reducer); + expect(element.dirty, true); + + await pump(); + + when(reducer.call(1, 'bar')).thenReturn(1); + + store.dispatch('bar'); + + verify(reducer.call(1, 'bar')).called(1); + verifyNoMoreInteractions(reducer); + expect(element.dirty, false); + }); + + testWidgets('reducer required', (tester) async { + await expectPump( + () => tester.pumpWidget(HookBuilder( + builder: (context) { + useReducer(null); + return Container(); + }, + )), + throwsAssertionError, + ); + }); + + testWidgets('dispatch during build fails', (tester) async { + final reducer = Func2(); + + await expectPump( + () => tester.pumpWidget(HookBuilder( + builder: (context) { + useReducer(reducer.call).dispatch('Foo'); + return Container(); + }, + )), + throwsAssertionError, + ); + }); + testWidgets('first reducer call receive initialAction and initialState', + (tester) async { + final reducer = Func2(); + + when(reducer.call(0, 'Foo')).thenReturn(0); + await expectPump( + () => tester.pumpWidget(HookBuilder( + builder: (context) { + useReducer( + reducer.call, + initialAction: 'Foo', + initialState: 0, + ); + return Container(); + }, + )), + completes, + ); + }); + testWidgets('dispatchs reducer call must not return null', (tester) async { + final reducer = Func2(); + + Store store; + pump() => tester.pumpWidget(HookBuilder( + builder: (context) { + store = useReducer(reducer.call); + return Container(); + }, + )); + + when(reducer.call(null, null)).thenReturn(42); + + await pump(); + + when(reducer.call(42, 'foo')).thenReturn(null); + expect(() => store.dispatch('foo'), throwsAssertionError); + + await pump(); + expect(store.state, 42); + }); + + testWidgets('first reducer call must not return null', (tester) async { + final reducer = Func2(); + + await expectPump( + () => tester.pumpWidget(HookBuilder( + builder: (context) { + useReducer(reducer.call); + return Container(); + }, + )), + throwsAssertionError, + ); + }); + }); +} From 485bd3105dc54f5b8740fa2ccf5a7646d30f1609 Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Fri, 28 Dec 2018 12:30:36 +0100 Subject: [PATCH 040/384] changelog --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index bb69e947..205a1ebb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ ## 0.2.0: - Made all existing hooks as static functions, and removed `HookContext`. The migration is as followed: + ```dart Widget build(HookContext context) { final state = context.useState(0); @@ -8,6 +9,7 @@ Widget build(HookContext context) { ``` becomes: + ```dart Widget build(BuildContext context) { final state = useState(0); @@ -15,6 +17,7 @@ Widget build(BuildContext context) { ``` - Introduced keys for hooks and applied them to hooks where it makes sense. +- Added `useReducer` for complex state. It is similar to `useState` but is being managed by a `Reducer` and can only be changed by dispatching an action. - fixes a bug where hot-reload without using hooks throwed an exception ## 0.1.0: From 6687320d4e754115bda728132da30a960c225664 Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Fri, 28 Dec 2018 12:38:09 +0100 Subject: [PATCH 041/384] readme --- README.md | 42 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/README.md b/README.md index 887c3313..0712fe85 100644 --- a/README.md +++ b/README.md @@ -323,6 +323,48 @@ class Counter extends HookWidget { } ``` +- useReducer + +An alternative to useState for more complex states. + +`useReducer` manages an read only state that can be updated by dispatching actions which are interpreted by a `Reducer`. + +The following makes a counter app with both a "+1" and "-1" button: + +```dart +class Counter extends HookWidget { + @override + Widget build(BuildContext context) { + final counter = useReducer(_counterReducer, initialState: 0); + + return Column( + children: [ + Text(counter.state.toString()), + IconButton( + icon: const Icon(Icons.add), + onPressed: () => counter.dispatch('increment'), + ), + IconButton( + icon: const Icon(Icons.remove), + onPressed: () => counter.dispatch('decrement'), + ), + ], + ); + } + + int _counterReducer(int state, String action) { + switch (action) { + case 'increment': + return state + 1; + case 'decrement': + return state - 1; + default: + return state; + } + } +} +``` + - useMemoized Takes a callback, calls it synchronously and returns its result. The result is then stored to that subsequent calls will return the same result without calling the callback. From 7cae664e9198fe7686cfeb0f6af5fc4ced45476f Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Wed, 2 Jan 2019 08:36:05 +0100 Subject: [PATCH 042/384] wip --- lib/flutter_hooks.dart | 1 - lib/src/framework.dart | 6 +++++ lib/src/hooks.dart | 58 +++++++++++++++++++++++++++++++++++++----- 3 files changed, 58 insertions(+), 7 deletions(-) diff --git a/lib/flutter_hooks.dart b/lib/flutter_hooks.dart index dbbf2167..7752ebd5 100644 --- a/lib/flutter_hooks.dart +++ b/lib/flutter_hooks.dart @@ -1,2 +1 @@ export 'package:flutter_hooks/src/framework.dart'; -export 'package:flutter_hooks/src/hooks.dart'; diff --git a/lib/src/framework.dart b/lib/src/framework.dart index 5630b6be..b90b1c90 100644 --- a/lib/src/framework.dart +++ b/lib/src/framework.dart @@ -1,5 +1,11 @@ +import 'dart:async'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/scheduler.dart'; import 'package:flutter/widgets.dart'; +part 'hooks.dart'; + /// [Hook] is similar to a [StatelessWidget], but is not associated /// to an [Element]. /// diff --git a/lib/src/hooks.dart b/lib/src/hooks.dart index 71246350..c77d991c 100644 --- a/lib/src/hooks.dart +++ b/lib/src/hooks.dart @@ -1,9 +1,4 @@ -import 'dart:async'; - -import 'package:flutter/foundation.dart'; -import 'package:flutter/scheduler.dart'; -import 'package:flutter/widgets.dart'; -import 'package:flutter_hooks/src/framework.dart'; +part of 'framework.dart'; /// A [HookWidget] that defer its [HookWidget.build] to a callback class HookBuilder extends HookWidget { @@ -126,6 +121,13 @@ T useMemoized(T Function() valueBuilder, [List keys = const []]) { )); } +/// Obtain the [BuildContext] of the currently builder [HookWidget]. +BuildContext useContext() { + assert(HookElement._currentContext != null, + '`useContext` can only be called from the build method of HookWidget'); + return HookElement._currentContext; +} + class _MemoizedHook extends Hook { final T Function() valueBuilder; @@ -779,3 +781,47 @@ class _StreamControllerHookState super.dispose(); } } + +// TODO: update documentation +/// Creates a [StreamController] automatically disposed. +/// +/// See also: +/// * [StreamController] +/// * [useStream] +ValueNotifier useValueNotifier([T intialData, List keys]) { + return Hook.use(_ValueNotifierHook( + initialData: intialData, + keys: keys, + )); +} + +class _ValueNotifierHook extends Hook> { + final T initialData; + + const _ValueNotifierHook({List keys, this.initialData}) : super(keys: keys); + + @override + _UseValueNotiferHookState createState() => _UseValueNotiferHookState(); +} + +class _UseValueNotiferHookState + extends HookState, _ValueNotifierHook> { + ValueNotifier notifier; + + @override + void initHook() { + super.initHook(); + notifier = ValueNotifier(hook.initialData); + } + + @override + ValueNotifier build(BuildContext context) { + return notifier; + } + + @override + void dispose() { + notifier.dispose(); + super.dispose(); + } +} From 844d0c667e1b530c0cf951d00976966905a55cca Mon Sep 17 00:00:00 2001 From: 0xflotus <0xflotus@gmail.com> Date: Thu, 3 Jan 2019 15:36:13 +0100 Subject: [PATCH 043/384] fixed subscription --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 0712fe85..f274daf8 100644 --- a/README.md +++ b/README.md @@ -106,7 +106,7 @@ Widget build(BuildContext context) { ## Principle -Similarily to `State`, hooks are stored on the `Element` of a `Widget`. But instead of having one `State`, the `Element` stores a `List`. Then to use a `Hook`, one must call `Hook.use`. +Similarly to `State`, hooks are stored on the `Element` of a `Widget`. But instead of having one `State`, the `Element` stores a `List`. Then to use a `Hook`, one must call `Hook.use`. The hook returned by `use` is based on the number of times it has been called. The first call returns the first hook; the second call returns the second hook, the third returns the third hook, ... @@ -292,7 +292,7 @@ The following call to `useEffect` subscribes to a `Stream` and cancel the subscr ```dart Stream stream; useEffect(() { - final subscribtion = stream.listen(print); + final subscription = stream.listen(print); // This will cancel the subscription when the widget is disposed // or if the callback is called again. return subscription.cancel; From edb1949ae14d41eaad2cdef18aa3ec548ca34237 Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Sun, 6 Jan 2019 22:16:50 +0100 Subject: [PATCH 044/384] 100% coverage (#29) --- CHANGELOG.md | 6 ++ README.md | 13 ++- lib/src/hooks.dart | 10 ++- test/use_context_test.dart | 33 ++++++++ test/use_value_notifier_test.dart | 136 ++++++++++++++++++++++++++++++ 5 files changed, 193 insertions(+), 5 deletions(-) create mode 100644 test/use_context_test.dart create mode 100644 test/use_value_notifier_test.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index 205a1ebb..22f7ada1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +## 0.3.0: + +- NEW: `useValueNotifier`, which creates a `ValueNotifier` similarly to `useState`. But without listening it. +This can be useful to have a more granular rebuild when combined to `useValueListenable`. +- NEW: `useContext`, which exposes the `BuildContext` of the currently building `HookWidget`. + ## 0.2.0: - Made all existing hooks as static functions, and removed `HookContext`. The migration is as followed: diff --git a/README.md b/README.md index f274daf8..ea1a23b8 100644 --- a/README.md +++ b/README.md @@ -365,6 +365,17 @@ class Counter extends HookWidget { } ``` +- useContext + +Returns the `BuildContext` of the currently building `HookWidget`. This is useful when writing custom hooks that want to manipulate the `BuildContext`. + +```dart +MyInheritedWidget useMyInheritedWidget() { + BuildContext context = useContext(); + return MyInheritedWidget.of(context); +} +``` + - useMemoized Takes a callback, calls it synchronously and returns its result. The result is then stored to that subsequent calls will return the same result without calling the callback. @@ -402,7 +413,7 @@ final colorTween = useValueChanged( AlwaysStoppedAnimation(color); ``` -- useAnimationController, useStreamController, useSingleTickerProvider +- useAnimationController, useStreamController, useSingleTickerProvider, useValueNotifier A set of hooks that handles the whole life-cycle of an object. These hooks will take care of both creating, disposing and updating the object. diff --git a/lib/src/hooks.dart b/lib/src/hooks.dart index c77d991c..04683a7f 100644 --- a/lib/src/hooks.dart +++ b/lib/src/hooks.dart @@ -782,12 +782,14 @@ class _StreamControllerHookState } } -// TODO: update documentation -/// Creates a [StreamController] automatically disposed. +/// Creates a [ValueNotifier] automatically disposed. +/// +/// As opposed to `useState`, this hook do not subscribes to [ValueNotifier]. +/// This allows a more granular rebuild. /// /// See also: -/// * [StreamController] -/// * [useStream] +/// * [ValueNotifier] +/// * [useValueListenable] ValueNotifier useValueNotifier([T intialData, List keys]) { return Hook.use(_ValueNotifierHook( initialData: intialData, diff --git a/test/use_context_test.dart b/test/use_context_test.dart new file mode 100644 index 00000000..8f19bdad --- /dev/null +++ b/test/use_context_test.dart @@ -0,0 +1,33 @@ +import 'package:flutter/widgets.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'mock.dart'; + +void main() { + group('useContext', () { + testWidgets('returns current BuildContext during build', (tester) async { + BuildContext res; + + await tester.pumpWidget(HookBuilder(builder: (context) { + res = useContext(); + return Container(); + })); + + final context = tester.firstElement(find.byType(HookBuilder)); + + expect(res, context); + }); + + testWidgets('crashed outside of build', (tester) async { + expect(useContext, throwsAssertionError); + await tester.pumpWidget(HookBuilder( + builder: (context) { + useContext(); + return Container(); + }, + )); + expect(useContext, throwsAssertionError); + }); + }); +} diff --git a/test/use_value_notifier_test.dart b/test/use_value_notifier_test.dart new file mode 100644 index 00000000..66a54520 --- /dev/null +++ b/test/use_value_notifier_test.dart @@ -0,0 +1,136 @@ +import 'package:flutter/widgets.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; + +import 'mock.dart'; + +void main() { + group('useValueNotifier', () { + testWidgets('useValueNotifier basic', (tester) async { + ValueNotifier state; + HookElement element; + final listener = Func0(); + + await tester.pumpWidget(HookBuilder( + builder: (context) { + element = context as HookElement; + state = useValueNotifier(42); + return Container(); + }, + )); + + state.addListener(listener.call); + + expect(state.value, 42); + expect(element.dirty, false); + verifyNoMoreInteractions(listener); + + await tester.pump(); + + verifyNoMoreInteractions(listener); + expect(state.value, 42); + expect(element.dirty, false); + + state.value++; + verify(listener.call()).called(1); + verifyNoMoreInteractions(listener); + expect(element.dirty, false); + await tester.pump(); + + expect(state.value, 43); + expect(element.dirty, false); + verifyNoMoreInteractions(listener); + + // dispose + await tester.pumpWidget(const SizedBox()); + + // ignore: invalid_use_of_protected_member + expect(() => state.hasListeners, throwsFlutterError); + }); + + testWidgets('no initial data', (tester) async { + ValueNotifier state; + HookElement element; + final listener = Func0(); + + await tester.pumpWidget(HookBuilder( + builder: (context) { + element = context as HookElement; + state = useValueNotifier(); + return Container(); + }, + )); + + state.addListener(listener.call); + + expect(state.value, null); + expect(element.dirty, false); + verifyNoMoreInteractions(listener); + + await tester.pump(); + + expect(state.value, null); + expect(element.dirty, false); + verifyNoMoreInteractions(listener); + + state.value = 43; + expect(element.dirty, false); + verify(listener.call()).called(1); + verifyNoMoreInteractions(listener); + await tester.pump(); + + expect(state.value, 43); + expect(element.dirty, false); + verifyNoMoreInteractions(listener); + + // dispose + await tester.pumpWidget(const SizedBox()); + + // ignore: invalid_use_of_protected_member + expect(() => state.hasListeners, throwsFlutterError); + }); + + testWidgets('creates new valuenotifier when key change', (tester) async { + ValueNotifier state; + ValueNotifier previous; + + await tester.pumpWidget(HookBuilder( + builder: (context) { + state = useValueNotifier(42); + return Container(); + }, + )); + + await tester.pumpWidget(HookBuilder( + builder: (context) { + previous = state; + state = useValueNotifier(42, [42]); + return Container(); + }, + )); + + expect(state, isNot(previous)); + }); + testWidgets('instance stays the same when key don\' change', + (tester) async { + ValueNotifier state; + ValueNotifier previous; + + await tester.pumpWidget(HookBuilder( + builder: (context) { + state = useValueNotifier(null, [42]); + return Container(); + }, + )); + + await tester.pumpWidget(HookBuilder( + builder: (context) { + previous = state; + state = useValueNotifier(42, [42]); + return Container(); + }, + )); + + expect(state, previous); + }); + }); +} From 0bc8b5d36b4bc6ef5fd1c9e05104e3e5e211013e Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Sun, 6 Jan 2019 22:28:37 +0100 Subject: [PATCH 045/384] bump-version --- CHANGELOG.md | 2 +- example/pubspec.lock | 2 +- pubspec.yaml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 22f7ada1..3f9e4864 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,4 @@ -## 0.3.0: +## 0.2.1: - NEW: `useValueNotifier`, which creates a `ValueNotifier` similarly to `useState`. But without listening it. This can be useful to have a more granular rebuild when combined to `useValueListenable`. diff --git a/example/pubspec.lock b/example/pubspec.lock index ca6c8c53..a4d91030 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -40,7 +40,7 @@ packages: path: ".." relative: true source: path - version: "0.2.0" + version: "0.2.1" flutter_test: dependency: "direct dev" description: flutter diff --git a/pubspec.yaml b/pubspec.yaml index 688c6e73..9076e6a7 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -3,7 +3,7 @@ description: A flutter implementation of React hooks. It adds a new kind of widg homepage: https://github.com/rrousselGit/flutter_hooks author: Remi Rousselet -version: 0.2.0 +version: 0.2.1 environment: sdk: ">=2.0.0-dev.68.0 <3.0.0" From ca928916032a892e790c21a580e5f94326d0a4f0 Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Thu, 10 Jan 2019 01:40:52 +0100 Subject: [PATCH 046/384] Add didBuild life-cycle on Hook feature: add didBuild life-cycle on Hook This is useful want we need to trigger side-effects on the widget tree (such as pushing routes), or when watching what happens during build (like FlutterError.onError). closes #30 --- CHANGELOG.md | 5 ++ lib/src/framework.dart | 19 ++++++ test/hook_widget_test.dart | 122 ++++++++++++++++++++++--------------- test/mock.dart | 10 +++ 4 files changed, 107 insertions(+), 49 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3f9e4864..6756098f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +## 0.3.0: + +- NEW: new life-cycle availble on `HookState`: `didBuild`. +This life-cycle is called synchronously right after `build` method of `HookWidget` finished. + ## 0.2.1: - NEW: `useValueNotifier`, which creates a `ValueNotifier` similarly to `useState`. But without listening it. diff --git a/lib/src/framework.dart b/lib/src/framework.dart index b90b1c90..3554d118 100644 --- a/lib/src/framework.dart +++ b/lib/src/framework.dart @@ -206,6 +206,10 @@ abstract class HookState> { @mustCallSuper void dispose() {} + /// Called synchronously after the [HookWidget.build] method finished + @protected + void didBuild() {} + /// Called everytimes the [HookState] is requested /// /// [build] is where an [HookState] may use other hooks. This restriction is made to ensure that hooks are unconditionally always requested @@ -282,6 +286,21 @@ This may happen if the call to `Hook.use` is made under some condition. _debugIsBuilding = false; return true; }()); + + if (_hooks != null) { + for (final hook in _hooks) { + try { + hook.didBuild(); + } catch (exception, stack) { + FlutterError.reportError(FlutterErrorDetails( + exception: exception, + stack: stack, + library: 'hooks library', + context: 'while calling `didBuild` on ${hook.runtimeType}', + )); + } + } + } } @override diff --git a/test/hook_widget_test.dart b/test/hook_widget_test.dart index 519a5b9e..f481cfdb 100644 --- a/test/hook_widget_test.dart +++ b/test/hook_widget_test.dart @@ -10,6 +10,7 @@ void main() { final dispose = Func0(); final initHook = Func0(); final didUpdateHook = Func1(); + final didBuild = Func0(); final builder = Func1(); final createHook = () => HookTest( @@ -17,11 +18,21 @@ void main() { dispose: dispose.call, didUpdateHook: didUpdateHook.call, initHook: initHook.call, + didBuild: didBuild, ); + void verifyNoMoreHookInteration() { + verifyNoMoreInteractions(build); + verifyNoMoreInteractions(didBuild); + verifyNoMoreInteractions(dispose); + verifyNoMoreInteractions(initHook); + verifyNoMoreInteractions(didUpdateHook); + } + tearDown(() { reset(builder); reset(build); + reset(didBuild); reset(dispose); reset(initHook); reset(didUpdateHook); @@ -67,6 +78,7 @@ void main() { didUpdateHook: didUpdateHook.call, initHook: initHook.call, keys: keys, + didBuild: didBuild, createStateFn: createState.call, )); return Container(); @@ -79,24 +91,18 @@ void main() { createState.call(), initHook.call(), build.call(context), + didBuild.call(), ]); - verifyNoMoreInteractions(createState); - verifyNoMoreInteractions(initHook); - verifyNoMoreInteractions(build); - verifyNoMoreInteractions(didUpdateHook); - verifyNoMoreInteractions(dispose); + verifyNoMoreHookInteration(); await tester.pumpWidget(HookBuilder(builder: builder.call)); verifyInOrder([ didUpdateHook.call(any), build.call(context), + didBuild.call(), ]); - verifyNoMoreInteractions(createState); - verifyNoMoreInteractions(initHook); - verifyNoMoreInteractions(build); - verifyNoMoreInteractions(didUpdateHook); - verifyNoMoreInteractions(dispose); + verifyNoMoreHookInteration(); // from null to array keys = []; @@ -106,13 +112,10 @@ void main() { dispose.call(), createState.call(), initHook.call(), - build.call(context) + build.call(context), + didBuild.call(), ]); - verifyNoMoreInteractions(createState); - verifyNoMoreInteractions(initHook); - verifyNoMoreInteractions(build); - verifyNoMoreInteractions(didUpdateHook); - verifyNoMoreInteractions(dispose); + verifyNoMoreHookInteration(); // array immutable keys.add(42); @@ -122,12 +125,9 @@ void main() { verifyInOrder([ didUpdateHook.call(any), build.call(context), + didBuild.call(), ]); - verifyNoMoreInteractions(createState); - verifyNoMoreInteractions(initHook); - verifyNoMoreInteractions(build); - verifyNoMoreInteractions(didUpdateHook); - verifyNoMoreInteractions(dispose); + verifyNoMoreHookInteration(); // new array but content equal keys = [42]; @@ -137,12 +137,9 @@ void main() { verifyInOrder([ didUpdateHook.call(any), build.call(context), + didBuild.call(), ]); - verifyNoMoreInteractions(createState); - verifyNoMoreInteractions(initHook); - verifyNoMoreInteractions(build); - verifyNoMoreInteractions(didUpdateHook); - verifyNoMoreInteractions(dispose); + verifyNoMoreHookInteration(); // new array new content keys = [44]; @@ -153,13 +150,10 @@ void main() { dispose.call(), createState.call(), initHook.call(), - build.call(context) + build.call(context), + didBuild.call() ]); - verifyNoMoreInteractions(createState); - verifyNoMoreInteractions(initHook); - verifyNoMoreInteractions(build); - verifyNoMoreInteractions(didUpdateHook); - verifyNoMoreInteractions(dispose); + verifyNoMoreHookInteration(); }); testWidgets('hook & setState', (tester) async { @@ -186,6 +180,45 @@ void main() { expect(hookContext.dirty, true); }); + testWidgets('didBuild called even if build crashed', (tester) async { + when(build.call(any)).thenThrow(42); + when(builder.call(any)).thenAnswer((invocation) { + Hook.use(createHook()); + return Container(); + }); + + await expectPump( + () => tester.pumpWidget(HookBuilder( + builder: builder.call, + )), + throwsA(42), + ); + + verify(didBuild.call()).called(1); + }); + testWidgets('all didBuild called even if one crashes', (tester) async { + final didBuild2 = Func0(); + + when(didBuild.call()).thenThrow(42); + when(builder.call(any)).thenAnswer((invocation) { + Hook.use(createHook()); + Hook.use(HookTest(didBuild: didBuild2)); + return Container(); + }); + + await expectPump( + () => tester.pumpWidget(HookBuilder( + builder: builder.call, + )), + throwsA(42), + ); + + verifyInOrder([ + didBuild.call(), + didBuild2.call(), + ]); + }); + testWidgets('life-cycles in order', (tester) async { int result; HookTest hook; @@ -206,9 +239,9 @@ void main() { verifyInOrder([ initHook.call(), build.call(context), + didBuild.call(), ]); - verifyZeroInteractions(didUpdateHook); - verifyZeroInteractions(dispose); + verifyNoMoreHookInteration(); when(build.call(context)).thenReturn(24); var previousHook = hook; @@ -218,28 +251,19 @@ void main() { )); expect(result, 24); - verifyInOrder([ - didUpdateHook.call(previousHook), - build.call(any), - ]); - verifyNoMoreInteractions(initHook); - verifyZeroInteractions(dispose); + verifyInOrder( + [didUpdateHook.call(previousHook), build.call(any), didBuild.call()]); + verifyNoMoreHookInteration(); previousHook = hook; await tester.pump(); - verifyNoMoreInteractions(initHook); - verifyNoMoreInteractions(didUpdateHook); - verifyNoMoreInteractions(build); - verifyZeroInteractions(dispose); + verifyNoMoreHookInteration(); await tester.pumpWidget(const SizedBox()); - verifyNoMoreInteractions(initHook); - verifyNoMoreInteractions(didUpdateHook); - verifyNoMoreInteractions(build); - verify(dispose.call()); - verifyNoMoreInteractions(dispose); + verify(dispose.call()).called(1); + verifyNoMoreHookInteration(); }); testWidgets('dispose all called even on failed', (tester) async { diff --git a/test/mock.dart b/test/mock.dart index 2ecdc574..70d553f3 100644 --- a/test/mock.dart +++ b/test/mock.dart @@ -28,6 +28,7 @@ class Func2 extends Mock implements _Func2 {} class HookTest extends Hook { final R Function(BuildContext context) build; final void Function() dispose; + final void Function() didBuild; final void Function() initHook; final void Function(HookTest previousHook) didUpdateHook; final HookStateTest Function() createStateFn; @@ -38,6 +39,7 @@ class HookTest extends Hook { this.initHook, this.didUpdateHook, this.createStateFn, + this.didBuild, List keys, }) : super(keys: keys); @@ -71,6 +73,14 @@ class HookStateTest extends HookState> { } } + @override + void didBuild() { + super.didBuild(); + if (hook.didBuild != null) { + hook.didBuild(); + } + } + @override R build(BuildContext context) { if (hook.build != null) { From 005126b037b0daaeab66369d1e41a5e81417493a Mon Sep 17 00:00:00 2001 From: Brian Egan Date: Mon, 14 Jan 2019 18:53:58 +0100 Subject: [PATCH 047/384] Example Gallery (#26) misc: Improved the example folder with a gallery showcasing the different possibilities. --- example/.gitignore | 2 - example/.metadata | 10 +++ example/README.md | 12 +++ example/lib/custom_hook_function.dart | 47 +++++++++++ example/lib/main.dart | 107 +++++++++++--------------- example/lib/use_effect.dart | 92 ++++++++++++++++++++++ example/lib/use_state.dart | 33 ++++++++ example/lib/use_stream.dart | 44 +++++++++++ example/pubspec.yaml | 9 +-- 9 files changed, 285 insertions(+), 71 deletions(-) create mode 100644 example/.metadata create mode 100644 example/README.md create mode 100644 example/lib/custom_hook_function.dart create mode 100644 example/lib/use_effect.dart create mode 100644 example/lib/use_state.dart create mode 100644 example/lib/use_stream.dart diff --git a/example/.gitignore b/example/.gitignore index 4f842a89..47e0b4d6 100644 --- a/example/.gitignore +++ b/example/.gitignore @@ -9,8 +9,6 @@ .buildlog/ .history .svn/ -/test -.metadata # IntelliJ related *.iml diff --git a/example/.metadata b/example/.metadata new file mode 100644 index 00000000..460bc20b --- /dev/null +++ b/example/.metadata @@ -0,0 +1,10 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: 5391447fae6209bb21a89e6a5a6583cac1af9b4b + channel: stable + +project_type: app diff --git a/example/README.md b/example/README.md new file mode 100644 index 00000000..5e4a954d --- /dev/null +++ b/example/README.md @@ -0,0 +1,12 @@ +# Flutter Hooks Gallery + +A series of examples demonstrating how to use Flutter Hooks! It teaches how to +use the Widgets and hooks that are provided by this library, as well examples +demonstrating how to write custom hooks. + +## Run the app + + 1. Open a terminal + 2. Navigate to this `example` directory + 3. Run `flutter create .` + 4. Run `flutter run` from your Terminal, or launch the project from your IDE! \ No newline at end of file diff --git a/example/lib/custom_hook_function.dart b/example/lib/custom_hook_function.dart new file mode 100644 index 00000000..cbdfb7d0 --- /dev/null +++ b/example/lib/custom_hook_function.dart @@ -0,0 +1,47 @@ +// ignore_for_file: omit_local_variable_types +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; + +/// This example demonstrates how to write a hook function that enhances the +/// useState hook with logging functionality. +class CustomHookFunctionExample extends HookWidget { + @override + Widget build(BuildContext context) { + // Next, invoke the custom `useLoggedState` hook with a default value to + // create a `counter` variable that contains a `value`. Whenever the value + // is changed, this Widget will be rebuilt and the result will be logged! + final counter = useLoggedState(0); + + return Scaffold( + appBar: AppBar( + title: const Text('Custom Hook: Function'), + ), + body: Center( + // Read the current value from the counter + child: Text('Button tapped ${counter.value} times'), + ), + floatingActionButton: FloatingActionButton( + child: const Icon(Icons.add), + // When the button is pressed, update the value of the counter! This + // will trigger a rebuild as well as printing the latest value to the + // console! + onPressed: () => counter.value++, + ), + ); + } +} + +/// A custom hook that wraps the useState hook to add logging. Hooks can be +/// composed -- meaning you can use hooks within hooks! +ValueNotifier useLoggedState([T initialData]) { + // First, call the useState hook. It will create a ValueNotifier for you that + // rebuilds the Widget whenever the value changes. + final result = useState(initialData); + + // Next, call the useValueChanged hook to print the state whenever it changes + useValueChanged(result.value, (T _, T __) { + print(result.value); + }); + + return result; +} diff --git a/example/lib/main.dart b/example/lib/main.dart index 2a40d539..5a28c85d 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -1,79 +1,60 @@ // ignore_for_file: omit_local_variable_types -import 'dart:async'; - import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:shared_preferences/shared_preferences.dart'; +import 'package:flutter_hooks_gallery/use_effect.dart'; +import 'package:flutter_hooks_gallery/use_state.dart'; +import 'package:flutter_hooks_gallery/use_stream.dart'; -void main() => runApp(_MyApp()); +void main() => runApp(HooksGalleryApp()); -class _MyApp extends StatelessWidget { +/// An App that demonstrates how to use hooks. It includes examples that cover +/// the hooks provided by this library as well as examples that demonstrate +/// how to write custom hooks. +class HooksGalleryApp extends StatelessWidget { @override Widget build(BuildContext context) { - return const MaterialApp( - title: 'Flutter Demo', - home: _Counter(), + return MaterialApp( + title: 'Flutter Hooks Gallery', + home: Scaffold( + appBar: AppBar( + title: const Text('Flutter Hooks Gallery'), + ), + body: ListView(children: [ + _GalleryItem( + title: 'useState', + builder: (context) => UseStateExample(), + ), + _GalleryItem( + title: 'useMemoize + useStream', + builder: (context) => UseStreamExample(), + ), + _GalleryItem( + title: 'Custom Hook Function', + builder: (context) => CustomHookExample(), + ), + ]), + ), ); } } -class _Counter extends HookWidget { - const _Counter({Key key}) : super(key: key); +class _GalleryItem extends StatelessWidget { + final String title; + final WidgetBuilder builder; + + const _GalleryItem({this.title, this.builder}); @override Widget build(BuildContext context) { - StreamController countController = - _useLocalStorageInt(context, 'counter'); - return Scaffold( - appBar: AppBar( - title: const Text('Counter app'), - ), - body: Center( - child: HookBuilder( - builder: (context) { - AsyncSnapshot count = useStream(countController.stream); - - return !count.hasData - // Currently loading value from local storage, or there's an error - ? const CircularProgressIndicator() - : GestureDetector( - onTap: () => countController.add(count.data + 1), - child: Text('You tapped me ${count.data} times.'), - ); - }, - ), - ), + return ListTile( + title: Text(title), + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: builder, + ), + ); + }, ); } } - -StreamController _useLocalStorageInt( - BuildContext context, - String key, { - int defaultValue = 0, -}) { - final controller = useStreamController(keys: [key]); - - // We define a callback that will be called on first build - // and whenever the controller/key change - useEffect(() { - // We listen to the data and push new values to local storage - final sub = controller.stream.listen((data) async { - final prefs = await SharedPreferences.getInstance(); - await prefs.setInt(key, data); - }); - // Unsubscribe when the widget is disposed - // or on controller/key change - return sub.cancel; - }, [controller, key]); - // We load the initial value - useEffect(() { - SharedPreferences.getInstance().then((prefs) async { - int valueFromStorage = prefs.getInt(key); - controller.add(valueFromStorage ?? defaultValue); - }).catchError(controller.addError); - // ensure the callback is called only on first build - }, [controller, key]); - - return controller; -} diff --git a/example/lib/use_effect.dart b/example/lib/use_effect.dart new file mode 100644 index 00000000..ad76aa36 --- /dev/null +++ b/example/lib/use_effect.dart @@ -0,0 +1,92 @@ +// ignore_for_file: omit_local_variable_types +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +/// This example demonstrates how to create a custom Hook. +class CustomHookExample extends HookWidget { + @override + Widget build(BuildContext context) { + // Consume the custom hook. It returns a StreamController that we can use + // within this Widget. + // + // To update the stored value, `add` data to the StreamController. To get + // the latest value from the StreamController, listen to the Stream with + // the useStream hook. + StreamController countController = _useLocalStorageInt('counter'); + + return Scaffold( + appBar: AppBar( + title: const Text('Custom Hook example'), + ), + body: Center( + // Use a HookBuilder Widget to listen to the Stream. This ensures a + // smaller portion of the Widget tree is rebuilt when the stream emits a + // new value + child: HookBuilder( + builder: (context) { + AsyncSnapshot count = useStream(countController.stream); + + return !count.hasData + ? const CircularProgressIndicator() + : GestureDetector( + onTap: () => countController.add(count.data + 1), + child: Text('You tapped me ${count.data} times.'), + ); + }, + ), + ), + ); + } +} + +// A custom hook that will read and write values to local storage using the +// SharedPreferences package. +StreamController _useLocalStorageInt( + String key, { + int defaultValue = 0, +}) { + // Custom hooks can use additional hooks internally! + final controller = useStreamController(keys: [key]); + + // Pass a callback to the useEffect hook. This function should be called on + // first build and every time the controller or key changes + useEffect( + () { + // Listen to the StreamController, and when a value is added, store it + // using SharedPrefs. + final sub = controller.stream.listen((data) async { + final prefs = await SharedPreferences.getInstance(); + await prefs.setInt(key, data); + }); + // Unsubscribe when the widget is disposed + // or on controller/key change + return sub.cancel; + }, + // Pass the controller and key to the useEffect hook. This will ensure the + // useEffect hook is only called the first build or when one of the the + // values changes. + [controller, key], + ); + + // Load the initial value from local storage and add it as the initial value + // to the controller + useEffect( + () { + SharedPreferences.getInstance().then((prefs) async { + int valueFromStorage = prefs.getInt(key); + controller.add(valueFromStorage ?? defaultValue); + }).catchError(controller.addError); + }, + // Pass the controller and key to the useEffect hook. This will ensure the + // useEffect hook is only called the first build or when one of the the + // values changes. + [controller, key], + ); + + // Finally, return the StreamController. This allows users to add values from + // the Widget layer and listen to the stream for changes. + return controller; +} diff --git a/example/lib/use_state.dart b/example/lib/use_state.dart new file mode 100644 index 00000000..ea62e6df --- /dev/null +++ b/example/lib/use_state.dart @@ -0,0 +1,33 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; + +/// This example emulates the basic Counter app generated by the +/// `flutter create` command to demonstrates the `useState` hook. +/// +/// First, instead of a StatefulWidget, use a HookWidget instead! +class UseStateExample extends HookWidget { + @override + Widget build(BuildContext context) { + // Next, invoke the `useState` function with a default value to create a + // `counter` variable that contains a `value`. Whenever the value is + // changed, this Widget will be rebuilt! + final counter = useState(0); + + return Scaffold( + appBar: AppBar( + title: const Text('useState example'), + ), + body: Center( + // Read the current value from the counter + child: Text('Button tapped ${counter.value} times'), + ), + floatingActionButton: FloatingActionButton( + child: const Icon(Icons.add), + // When the button is pressed, update the value of the counter! This + // will trigger a rebuild, displaying the latest value in the Text + // Widget above! + onPressed: () => counter.value++, + ), + ); + } +} diff --git a/example/lib/use_stream.dart b/example/lib/use_stream.dart new file mode 100644 index 00000000..87e242d5 --- /dev/null +++ b/example/lib/use_stream.dart @@ -0,0 +1,44 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; + +/// This example demonstrates how to use Hooks to rebuild a Widget whenever +/// a Stream emits a new value. +class UseStreamExample extends StatelessWidget { + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('useStream example'), + ), + body: Center( + // In this example, the Text Widget is the only portion that needs to + // rebuild when the Stream changes. Therefore, use a HookBuilder + // Widget to limit rebuilds to this section of the app, rather than + // marking the entire UseStreamExample as a HookWidget! + child: HookBuilder( + builder: (context) { + // First, create and cache a Stream with the `useMemoized` hook. + // This hook allows you to create an Object (such as a Stream or + // Future) the first time this builder function is invoked without + // recreating it on each subsequent build! + final stream = useMemoized( + () => Stream.periodic(Duration(seconds: 1), (i) => i + 1), + ); + // Next, invoke the `useStream` hook to listen for updates to the + // Stream. This triggers a rebuild whenever a new value is emitted. + // + // Like normal StreamBuilders, it returns the current AsyncSnapshot. + final snapshot = useStream(stream); + + // Finally, use the data from the Stream to render a text Widget. + // If no data is available, fallback to a default value. + return Text( + '${snapshot.data ?? 0}', + style: const TextStyle(fontSize: 36), + ); + }, + ), + ), + ); + } +} diff --git a/example/pubspec.yaml b/example/pubspec.yaml index a9f91f03..8698db64 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -1,4 +1,4 @@ -name: example +name: flutter_hooks_gallery description: A new Flutter project. version: 1.0.0+1 @@ -9,16 +9,13 @@ environment: dependencies: flutter: sdk: flutter - flutter_hooks: 0.0.1 + flutter_hooks: + path: ../ shared_preferences: ^0.4.3 dev_dependencies: flutter_test: sdk: flutter -dependency_overrides: - flutter_hooks: - path: ../ - flutter: uses-material-design: true From cedbcf9425dad153930442ca7faad08ea476e379 Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Wed, 30 Jan 2019 17:36:15 +0100 Subject: [PATCH 048/384] didBuild runs after build but before child build (#42) --- example/lib/use_stream.dart | 2 + example/pubspec.lock | 15 +++-- lib/src/framework.dart | 86 +++++++++++++------------ pubspec.lock | 8 +-- test/hook_widget_test.dart | 29 +++++++-- test/use_animation_controller_test.dart | 8 +-- test/use_value_changed_test.dart | 2 +- 7 files changed, 90 insertions(+), 60 deletions(-) diff --git a/example/lib/use_stream.dart b/example/lib/use_stream.dart index 87e242d5..b54fb121 100644 --- a/example/lib/use_stream.dart +++ b/example/lib/use_stream.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; diff --git a/example/pubspec.lock b/example/pubspec.lock index a4d91030..a9e6d776 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -67,6 +67,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.6.2" + pedantic: + dependency: transitive + description: + name: pedantic + url: "https://pub.dartlang.org" + source: hosted + version: "1.4.0" quiver: dependency: transitive description: @@ -92,7 +99,7 @@ packages: name: source_span url: "https://pub.dartlang.org" source: hosted - version: "1.4.1" + version: "1.5.3" stack_trace: dependency: transitive description: @@ -120,14 +127,14 @@ packages: name: term_glyph url: "https://pub.dartlang.org" source: hosted - version: "1.0.1" + version: "1.1.0" test_api: dependency: transitive description: name: test_api url: "https://pub.dartlang.org" source: hosted - version: "0.2.1" + version: "0.2.2" typed_data: dependency: transitive description: @@ -143,5 +150,5 @@ packages: source: hosted version: "2.0.8" sdks: - dart: ">=2.0.0 <3.0.0" + dart: ">=2.1.0 <3.0.0" flutter: ">=0.1.4 <2.0.0" diff --git a/lib/src/framework.dart b/lib/src/framework.dart index 3554d118..80d0e766 100644 --- a/lib/src/framework.dart +++ b/lib/src/framework.dart @@ -249,55 +249,59 @@ class HookElement extends StatefulElement { HookWidget get widget => super.widget as HookWidget; @override - void performRebuild() { - _currentHook = _hooks?.iterator; - // first iterator always has null - _currentHook?.moveNext(); - _hookIndex = 0; - assert(() { - _debugShouldDispose = false; - _isFirstBuild ??= true; - _didReassemble ??= false; - _debugIsBuilding = true; - return true; - }()); - HookElement._currentContext = this; - super.performRebuild(); - HookElement._currentContext = null; + Widget build() { + try { + _currentHook = _hooks?.iterator; + // first iterator always has null + _currentHook?.moveNext(); + _hookIndex = 0; + assert(() { + _debugShouldDispose = false; + _isFirstBuild ??= true; + _didReassemble ??= false; + _debugIsBuilding = true; + return true; + }()); + HookElement._currentContext = this; + final result = super.build(); + HookElement._currentContext = null; - // dispose removed items - assert(() { - if (_didReassemble && _hooks != null) { - for (var i = _hookIndex; i < _hooks.length;) { - _hooks.removeAt(i).dispose(); + // dispose removed items + assert(() { + if (_didReassemble && _hooks != null) { + for (var i = _hookIndex; i < _hooks.length;) { + _hooks.removeAt(i).dispose(); + } } - } - return true; - }()); - assert(_hookIndex == (_hooks?.length ?? 0), ''' + return true; + }()); + assert(_hookIndex == (_hooks?.length ?? 0), ''' Build for $widget finished with less hooks used than a previous build. Used $_hookIndex hooks while a previous build had ${_hooks.length}. This may happen if the call to `Hook.use` is made under some condition. '''); - assert(() { - _isFirstBuild = false; - _didReassemble = false; - _debugIsBuilding = false; - return true; - }()); + assert(() { + _isFirstBuild = false; + _didReassemble = false; + _debugIsBuilding = false; + return true; + }()); - if (_hooks != null) { - for (final hook in _hooks) { - try { - hook.didBuild(); - } catch (exception, stack) { - FlutterError.reportError(FlutterErrorDetails( - exception: exception, - stack: stack, - library: 'hooks library', - context: 'while calling `didBuild` on ${hook.runtimeType}', - )); + return result; + } finally { + if (_hooks != null) { + for (final hook in _hooks) { + try { + hook.didBuild(); + } catch (exception, stack) { + FlutterError.reportError(FlutterErrorDetails( + exception: exception, + stack: stack, + library: 'hooks library', + context: 'while calling `didBuild` on ${hook.runtimeType}', + )); + } } } } diff --git a/pubspec.lock b/pubspec.lock index ca8ec7c4..9507501a 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -92,7 +92,7 @@ packages: name: source_span url: "https://pub.dartlang.org" source: hosted - version: "1.4.1" + version: "1.5.3" stack_trace: dependency: transitive description: @@ -120,14 +120,14 @@ packages: name: term_glyph url: "https://pub.dartlang.org" source: hosted - version: "1.0.1" + version: "1.1.0" test_api: dependency: transitive description: name: test_api url: "https://pub.dartlang.org" source: hosted - version: "0.2.1" + version: "0.2.2" typed_data: dependency: transitive description: @@ -143,4 +143,4 @@ packages: source: hosted version: "2.0.8" sdks: - dart: ">=2.0.0 <3.0.0" + dart: ">=2.1.0 <3.0.0" diff --git a/test/hook_widget_test.dart b/test/hook_widget_test.dart index f481cfdb..9643916d 100644 --- a/test/hook_widget_test.dart +++ b/test/hook_widget_test.dart @@ -85,7 +85,7 @@ void main() { }); await tester.pumpWidget(HookBuilder(builder: builder.call)); - final HookElement context = find.byType(HookBuilder).evaluate().first; + final context = find.byType(HookBuilder).evaluate().first; verifyInOrder([ createState.call(), @@ -219,6 +219,23 @@ void main() { ]); }); + testWidgets('calls didBuild before building children', (tester) async { + final buildChild = Func1(); + when(buildChild.call(any)).thenReturn(Container()); + + await tester.pumpWidget(HookBuilder( + builder: (context) { + Hook.use(createHook()); + return Builder(builder: buildChild); + }, + )); + + verifyInOrder([ + didBuild(), + buildChild.call(any), + ]); + }); + testWidgets('life-cycles in order', (tester) async { int result; HookTest hook; @@ -430,7 +447,7 @@ void main() { await tester.pumpWidget(HookBuilder(builder: builder.call)); - final HookElement context = find.byType(HookBuilder).evaluate().first; + final context = find.byType(HookBuilder).evaluate().first; verifyInOrder([ initHook.call(), @@ -479,7 +496,7 @@ void main() { await tester.pumpWidget(HookBuilder(builder: builder.call)); - final HookElement context = find.byType(HookBuilder).evaluate().first; + final context = find.byType(HookBuilder).evaluate().first; verifyInOrder([ initHook.call(), @@ -532,7 +549,7 @@ void main() { }); await tester.pumpWidget(HookBuilder(builder: builder.call)); - final HookElement context = find.byType(HookBuilder).evaluate().first; + final context = find.byType(HookBuilder).evaluate().first; verifyInOrder([ initHook.call(), @@ -594,7 +611,7 @@ void main() { await tester.pumpWidget(HookBuilder(builder: builder.call)); - final HookElement context = find.byType(HookBuilder).evaluate().first; + final context = find.byType(HookBuilder).evaluate().first; // We don't care about datas of the first render clearInteractions(initHook); @@ -689,7 +706,7 @@ void main() { await tester.pumpWidget(HookBuilder(builder: builder.call)); - final HookElement context = find.byType(HookBuilder).evaluate().first; + final context = find.byType(HookBuilder).evaluate().first; // We don't care about datas of the first render clearInteractions(initHook); diff --git a/test/use_animation_controller_test.dart b/test/use_animation_controller_test.dart index b3e10777..04f77a92 100644 --- a/test/use_animation_controller_test.dart +++ b/test/use_animation_controller_test.dart @@ -37,8 +37,8 @@ void main() { TickerProvider provider; provider = _TickerProvider(); when(provider.createTicker(any)).thenAnswer((_) { - void Function(Duration) cb = _.positionalArguments[0]; - return tester.createTicker(cb); + return tester + .createTicker(_.positionalArguments[0] as void Function(Duration)); }); await tester.pumpWidget( @@ -71,8 +71,8 @@ void main() { var previousController = controller; provider = _TickerProvider(); when(provider.createTicker(any)).thenAnswer((_) { - void Function(Duration) cb = _.positionalArguments[0]; - return tester.createTicker(cb); + return tester + .createTicker(_.positionalArguments[0] as void Function(Duration)); }); await tester.pumpWidget( diff --git a/test/use_value_changed_test.dart b/test/use_value_changed_test.dart index 19a6db93..5bfd4a11 100644 --- a/test/use_value_changed_test.dart +++ b/test/use_value_changed_test.dart @@ -17,7 +17,7 @@ void main() { )); await pump(); - final HookElement context = find.byType(HookBuilder).evaluate().first; + final context = find.byType(HookBuilder).evaluate().first; expect(result, null); verifyNoMoreInteractions(_useValueChanged); From 591b5c33d30aa48a8c7b4340e127a986e036119a Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Wed, 30 Jan 2019 18:14:34 +0100 Subject: [PATCH 049/384] revert didBuild (#45) --- CHANGELOG.md | 5 -- example/lib/use_stream.dart | 2 - example/pubspec.lock | 2 +- example/pubspec.yaml | 2 +- lib/src/framework.dart | 83 ++++++++------------- pubspec.lock | 8 +-- test/hook_widget_test.dart | 139 +++++++++++++----------------------- test/mock.dart | 10 --- 8 files changed, 85 insertions(+), 166 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6756098f..3f9e4864 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,3 @@ -## 0.3.0: - -- NEW: new life-cycle availble on `HookState`: `didBuild`. -This life-cycle is called synchronously right after `build` method of `HookWidget` finished. - ## 0.2.1: - NEW: `useValueNotifier`, which creates a `ValueNotifier` similarly to `useState`. But without listening it. diff --git a/example/lib/use_stream.dart b/example/lib/use_stream.dart index b54fb121..87e242d5 100644 --- a/example/lib/use_stream.dart +++ b/example/lib/use_stream.dart @@ -1,5 +1,3 @@ -import 'dart:async'; - import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; diff --git a/example/pubspec.lock b/example/pubspec.lock index a9e6d776..26ebf7a2 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -99,7 +99,7 @@ packages: name: source_span url: "https://pub.dartlang.org" source: hosted - version: "1.5.3" + version: "1.5.4" stack_trace: dependency: transitive description: diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 8698db64..c66951d2 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -4,7 +4,7 @@ description: A new Flutter project. version: 1.0.0+1 environment: - sdk: ">=2.0.0-dev.68.0 <3.0.0" + sdk: ">=2.1.0 <3.0.0" dependencies: flutter: diff --git a/lib/src/framework.dart b/lib/src/framework.dart index 80d0e766..b90b1c90 100644 --- a/lib/src/framework.dart +++ b/lib/src/framework.dart @@ -206,10 +206,6 @@ abstract class HookState> { @mustCallSuper void dispose() {} - /// Called synchronously after the [HookWidget.build] method finished - @protected - void didBuild() {} - /// Called everytimes the [HookState] is requested /// /// [build] is where an [HookState] may use other hooks. This restriction is made to ensure that hooks are unconditionally always requested @@ -249,62 +245,43 @@ class HookElement extends StatefulElement { HookWidget get widget => super.widget as HookWidget; @override - Widget build() { - try { - _currentHook = _hooks?.iterator; - // first iterator always has null - _currentHook?.moveNext(); - _hookIndex = 0; - assert(() { - _debugShouldDispose = false; - _isFirstBuild ??= true; - _didReassemble ??= false; - _debugIsBuilding = true; - return true; - }()); - HookElement._currentContext = this; - final result = super.build(); - HookElement._currentContext = null; + void performRebuild() { + _currentHook = _hooks?.iterator; + // first iterator always has null + _currentHook?.moveNext(); + _hookIndex = 0; + assert(() { + _debugShouldDispose = false; + _isFirstBuild ??= true; + _didReassemble ??= false; + _debugIsBuilding = true; + return true; + }()); + HookElement._currentContext = this; + super.performRebuild(); + HookElement._currentContext = null; - // dispose removed items - assert(() { - if (_didReassemble && _hooks != null) { - for (var i = _hookIndex; i < _hooks.length;) { - _hooks.removeAt(i).dispose(); - } + // dispose removed items + assert(() { + if (_didReassemble && _hooks != null) { + for (var i = _hookIndex; i < _hooks.length;) { + _hooks.removeAt(i).dispose(); } - return true; - }()); - assert(_hookIndex == (_hooks?.length ?? 0), ''' + } + return true; + }()); + assert(_hookIndex == (_hooks?.length ?? 0), ''' Build for $widget finished with less hooks used than a previous build. Used $_hookIndex hooks while a previous build had ${_hooks.length}. This may happen if the call to `Hook.use` is made under some condition. '''); - assert(() { - _isFirstBuild = false; - _didReassemble = false; - _debugIsBuilding = false; - return true; - }()); - - return result; - } finally { - if (_hooks != null) { - for (final hook in _hooks) { - try { - hook.didBuild(); - } catch (exception, stack) { - FlutterError.reportError(FlutterErrorDetails( - exception: exception, - stack: stack, - library: 'hooks library', - context: 'while calling `didBuild` on ${hook.runtimeType}', - )); - } - } - } - } + assert(() { + _isFirstBuild = false; + _didReassemble = false; + _debugIsBuilding = false; + return true; + }()); } @override diff --git a/pubspec.lock b/pubspec.lock index 9507501a..ca8ec7c4 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -92,7 +92,7 @@ packages: name: source_span url: "https://pub.dartlang.org" source: hosted - version: "1.5.3" + version: "1.4.1" stack_trace: dependency: transitive description: @@ -120,14 +120,14 @@ packages: name: term_glyph url: "https://pub.dartlang.org" source: hosted - version: "1.1.0" + version: "1.0.1" test_api: dependency: transitive description: name: test_api url: "https://pub.dartlang.org" source: hosted - version: "0.2.2" + version: "0.2.1" typed_data: dependency: transitive description: @@ -143,4 +143,4 @@ packages: source: hosted version: "2.0.8" sdks: - dart: ">=2.1.0 <3.0.0" + dart: ">=2.0.0 <3.0.0" diff --git a/test/hook_widget_test.dart b/test/hook_widget_test.dart index 9643916d..575f6ad6 100644 --- a/test/hook_widget_test.dart +++ b/test/hook_widget_test.dart @@ -10,7 +10,6 @@ void main() { final dispose = Func0(); final initHook = Func0(); final didUpdateHook = Func1(); - final didBuild = Func0(); final builder = Func1(); final createHook = () => HookTest( @@ -18,21 +17,11 @@ void main() { dispose: dispose.call, didUpdateHook: didUpdateHook.call, initHook: initHook.call, - didBuild: didBuild, ); - void verifyNoMoreHookInteration() { - verifyNoMoreInteractions(build); - verifyNoMoreInteractions(didBuild); - verifyNoMoreInteractions(dispose); - verifyNoMoreInteractions(initHook); - verifyNoMoreInteractions(didUpdateHook); - } - tearDown(() { reset(builder); reset(build); - reset(didBuild); reset(dispose); reset(initHook); reset(didUpdateHook); @@ -78,7 +67,6 @@ void main() { didUpdateHook: didUpdateHook.call, initHook: initHook.call, keys: keys, - didBuild: didBuild, createStateFn: createState.call, )); return Container(); @@ -91,18 +79,24 @@ void main() { createState.call(), initHook.call(), build.call(context), - didBuild.call(), ]); - verifyNoMoreHookInteration(); + verifyNoMoreInteractions(createState); + verifyNoMoreInteractions(initHook); + verifyNoMoreInteractions(build); + verifyNoMoreInteractions(didUpdateHook); + verifyNoMoreInteractions(dispose); await tester.pumpWidget(HookBuilder(builder: builder.call)); verifyInOrder([ didUpdateHook.call(any), build.call(context), - didBuild.call(), ]); - verifyNoMoreHookInteration(); + verifyNoMoreInteractions(createState); + verifyNoMoreInteractions(initHook); + verifyNoMoreInteractions(build); + verifyNoMoreInteractions(didUpdateHook); + verifyNoMoreInteractions(dispose); // from null to array keys = []; @@ -112,10 +106,13 @@ void main() { dispose.call(), createState.call(), initHook.call(), - build.call(context), - didBuild.call(), + build.call(context) ]); - verifyNoMoreHookInteration(); + verifyNoMoreInteractions(createState); + verifyNoMoreInteractions(initHook); + verifyNoMoreInteractions(build); + verifyNoMoreInteractions(didUpdateHook); + verifyNoMoreInteractions(dispose); // array immutable keys.add(42); @@ -125,9 +122,12 @@ void main() { verifyInOrder([ didUpdateHook.call(any), build.call(context), - didBuild.call(), ]); - verifyNoMoreHookInteration(); + verifyNoMoreInteractions(createState); + verifyNoMoreInteractions(initHook); + verifyNoMoreInteractions(build); + verifyNoMoreInteractions(didUpdateHook); + verifyNoMoreInteractions(dispose); // new array but content equal keys = [42]; @@ -137,9 +137,12 @@ void main() { verifyInOrder([ didUpdateHook.call(any), build.call(context), - didBuild.call(), ]); - verifyNoMoreHookInteration(); + verifyNoMoreInteractions(createState); + verifyNoMoreInteractions(initHook); + verifyNoMoreInteractions(build); + verifyNoMoreInteractions(didUpdateHook); + verifyNoMoreInteractions(dispose); // new array new content keys = [44]; @@ -150,10 +153,13 @@ void main() { dispose.call(), createState.call(), initHook.call(), - build.call(context), - didBuild.call() + build.call(context) ]); - verifyNoMoreHookInteration(); + verifyNoMoreInteractions(createState); + verifyNoMoreInteractions(initHook); + verifyNoMoreInteractions(build); + verifyNoMoreInteractions(didUpdateHook); + verifyNoMoreInteractions(dispose); }); testWidgets('hook & setState', (tester) async { @@ -180,62 +186,6 @@ void main() { expect(hookContext.dirty, true); }); - testWidgets('didBuild called even if build crashed', (tester) async { - when(build.call(any)).thenThrow(42); - when(builder.call(any)).thenAnswer((invocation) { - Hook.use(createHook()); - return Container(); - }); - - await expectPump( - () => tester.pumpWidget(HookBuilder( - builder: builder.call, - )), - throwsA(42), - ); - - verify(didBuild.call()).called(1); - }); - testWidgets('all didBuild called even if one crashes', (tester) async { - final didBuild2 = Func0(); - - when(didBuild.call()).thenThrow(42); - when(builder.call(any)).thenAnswer((invocation) { - Hook.use(createHook()); - Hook.use(HookTest(didBuild: didBuild2)); - return Container(); - }); - - await expectPump( - () => tester.pumpWidget(HookBuilder( - builder: builder.call, - )), - throwsA(42), - ); - - verifyInOrder([ - didBuild.call(), - didBuild2.call(), - ]); - }); - - testWidgets('calls didBuild before building children', (tester) async { - final buildChild = Func1(); - when(buildChild.call(any)).thenReturn(Container()); - - await tester.pumpWidget(HookBuilder( - builder: (context) { - Hook.use(createHook()); - return Builder(builder: buildChild); - }, - )); - - verifyInOrder([ - didBuild(), - buildChild.call(any), - ]); - }); - testWidgets('life-cycles in order', (tester) async { int result; HookTest hook; @@ -256,9 +206,9 @@ void main() { verifyInOrder([ initHook.call(), build.call(context), - didBuild.call(), ]); - verifyNoMoreHookInteration(); + verifyZeroInteractions(didUpdateHook); + verifyZeroInteractions(dispose); when(build.call(context)).thenReturn(24); var previousHook = hook; @@ -268,19 +218,28 @@ void main() { )); expect(result, 24); - verifyInOrder( - [didUpdateHook.call(previousHook), build.call(any), didBuild.call()]); - verifyNoMoreHookInteration(); + verifyInOrder([ + didUpdateHook.call(previousHook), + build.call(any), + ]); + verifyNoMoreInteractions(initHook); + verifyZeroInteractions(dispose); previousHook = hook; await tester.pump(); - verifyNoMoreHookInteration(); + verifyNoMoreInteractions(initHook); + verifyNoMoreInteractions(didUpdateHook); + verifyNoMoreInteractions(build); + verifyZeroInteractions(dispose); await tester.pumpWidget(const SizedBox()); - verify(dispose.call()).called(1); - verifyNoMoreHookInteration(); + verifyNoMoreInteractions(initHook); + verifyNoMoreInteractions(didUpdateHook); + verifyNoMoreInteractions(build); + verify(dispose.call()); + verifyNoMoreInteractions(dispose); }); testWidgets('dispose all called even on failed', (tester) async { diff --git a/test/mock.dart b/test/mock.dart index 70d553f3..2ecdc574 100644 --- a/test/mock.dart +++ b/test/mock.dart @@ -28,7 +28,6 @@ class Func2 extends Mock implements _Func2 {} class HookTest extends Hook { final R Function(BuildContext context) build; final void Function() dispose; - final void Function() didBuild; final void Function() initHook; final void Function(HookTest previousHook) didUpdateHook; final HookStateTest Function() createStateFn; @@ -39,7 +38,6 @@ class HookTest extends Hook { this.initHook, this.didUpdateHook, this.createStateFn, - this.didBuild, List keys, }) : super(keys: keys); @@ -73,14 +71,6 @@ class HookStateTest extends HookState> { } } - @override - void didBuild() { - super.didBuild(); - if (hook.didBuild != null) { - hook.didBuild(); - } - } - @override R build(BuildContext context) { if (hook.build != null) { From 998596268052522144541d7c9e483ceb8b401f62 Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Fri, 1 Feb 2019 19:54:25 +0100 Subject: [PATCH 050/384] NEW: `useStream` and `useFuture` now have an optional `preserveState` flag. (#49) This toggle how these hooks behave when changing the stream/future: If true (default) they keep the previous value, else they reset to initialState. --- CHANGELOG.md | 10 +++++++-- lib/src/hooks.dart | 37 +++++++++++++++++++++++------- test/use_future_test.dart | 45 +++++++++++++++++++++++++++++++++++++ test/use_stream_test.dart | 47 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 129 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3f9e4864..6733cffc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,14 @@ +## 0.3.0: + +- NEW: `useStream` and `useFuture` now have an optional `preserveState` flag. + This toggle how these hooks behaves when changing the stream/future: + If true (default) they keep the previous value, else they reset to initialState. + ## 0.2.1: - NEW: `useValueNotifier`, which creates a `ValueNotifier` similarly to `useState`. But without listening it. -This can be useful to have a more granular rebuild when combined to `useValueListenable`. -- NEW: `useContext`, which exposes the `BuildContext` of the currently building `HookWidget`. + This can be useful to have a more granular rebuild when combined to `useValueListenable`. +- NEW: `useContext`, which exposes the `BuildContext` of the currently building `HookWidget`. ## 0.2.0: diff --git a/lib/src/hooks.dart b/lib/src/hooks.dart index 04683a7f..0f7b406a 100644 --- a/lib/src/hooks.dart +++ b/lib/src/hooks.dart @@ -474,18 +474,24 @@ class _ListenableStateHook extends HookState { /// Subscribes to a [Future] and return its current state in an [AsyncSnapshot]. /// +/// * [preserveState] defines if the current value should be preserved when changing +/// the [Future] instance. +/// /// See also: /// * [Future] /// * [useValueListenable], [useListenable], [useAnimation] -AsyncSnapshot useFuture(Future future, {T initialData}) { - return Hook.use(_FutureHook(future, initialData: initialData)); +AsyncSnapshot useFuture(Future future, + {T initialData, bool preserveState = true}) { + return Hook.use(_FutureHook(future, + initialData: initialData, preserveState: preserveState)); } class _FutureHook extends Hook> { final Future future; + final bool preserveState; final T initialData; - const _FutureHook(this.future, {this.initialData}); + const _FutureHook(this.future, {this.initialData, this.preserveState = true}); @override _FutureStateHook createState() => _FutureStateHook(); @@ -512,7 +518,12 @@ class _FutureStateHook extends HookState, _FutureHook> { if (oldHook.future != hook.future) { if (_activeCallbackIdentity != null) { _unsubscribe(); - _snapshot = _snapshot.inState(ConnectionState.none); + if (hook.preserveState) { + _snapshot = _snapshot.inState(ConnectionState.none); + } else { + _snapshot = + AsyncSnapshot.withData(ConnectionState.none, hook.initialData); + } } _subscribe(); } @@ -560,15 +571,21 @@ class _FutureStateHook extends HookState, _FutureHook> { /// See also: /// * [Stream] /// * [useValueListenable], [useListenable], [useAnimation] -AsyncSnapshot useStream(Stream stream, {T initialData}) { - return Hook.use(_StreamHook(stream, initialData: initialData)); +AsyncSnapshot useStream(Stream stream, + {T initialData, bool preserveState = true}) { + return Hook.use(_StreamHook( + stream, + initialData: initialData, + preserveState: preserveState, + )); } class _StreamHook extends Hook> { final Stream stream; final T initialData; + final bool preserveState; - _StreamHook(this.stream, {this.initialData}); + _StreamHook(this.stream, {this.initialData, this.preserveState = true}); @override _StreamHookState createState() => _StreamHookState(); @@ -592,7 +609,11 @@ class _StreamHookState extends HookState, _StreamHook> { if (oldWidget.stream != hook.stream) { if (_subscription != null) { _unsubscribe(); - _summary = afterDisconnected(_summary); + if (hook.preserveState) { + _summary = afterDisconnected(_summary); + } else { + _summary = initial(); + } } _subscribe(); } diff --git a/test/use_future_test.dart b/test/use_future_test.dart index 0554e760..89389e4a 100644 --- a/test/use_future_test.dart +++ b/test/use_future_test.dart @@ -6,6 +6,51 @@ import 'package:flutter_hooks/flutter_hooks.dart'; import 'mock.dart'; void main() { + testWidgets('default preserve state, changing future keeps previous value', + (tester) async { + AsyncSnapshot value; + Widget Function(BuildContext) builder(Future stream) { + return (context) { + value = useFuture(stream); + return Container(); + }; + } + + var future = Future.value(0); + await tester.pumpWidget(HookBuilder(builder: builder(future))); + expect(value.data, null); + await tester.pumpWidget(HookBuilder(builder: builder(future))); + expect(value.data, 0); + + future = Future.value(42); + await tester.pumpWidget(HookBuilder(builder: builder(future))); + expect(value.data, 0); + await tester.pumpWidget(HookBuilder(builder: builder(future))); + expect(value.data, 42); + }); + testWidgets('If preserveState == false, changing future resets value', + (tester) async { + AsyncSnapshot value; + Widget Function(BuildContext) builder(Future stream) { + return (context) { + value = useFuture(stream, preserveState: false); + return Container(); + }; + } + + var future = Future.value(0); + await tester.pumpWidget(HookBuilder(builder: builder(future))); + expect(value.data, null); + await tester.pumpWidget(HookBuilder(builder: builder(future))); + expect(value.data, 0); + + future = Future.value(42); + await tester.pumpWidget(HookBuilder(builder: builder(future))); + expect(value.data, null); + await tester.pumpWidget(HookBuilder(builder: builder(future))); + expect(value.data, 42); + }); + Widget Function(BuildContext) snapshotText(Future stream, {String initialData}) { return (context) { diff --git a/test/use_stream_test.dart b/test/use_stream_test.dart index 63c46dd9..ee677b0d 100644 --- a/test/use_stream_test.dart +++ b/test/use_stream_test.dart @@ -8,6 +8,53 @@ import 'mock.dart'; /// port of [StreamBuilder] /// void main() { + testWidgets('default preserve state, changing stream keeps previous value', + (tester) async { + AsyncSnapshot value; + Widget Function(BuildContext) builder(Stream stream) { + return (context) { + value = useStream(stream); + return Container(); + }; + } + + var stream = Stream.fromFuture(Future.value(0)); + await tester.pumpWidget(HookBuilder(builder: builder(stream))); + expect(value.data, null); + await tester.pumpWidget(HookBuilder(builder: builder(stream))); + expect(value.data, 0); + + stream = Stream.fromFuture(Future.value(42)); + await tester.pumpWidget(HookBuilder(builder: builder(stream))); + expect(value.data, 0); + await tester.pumpWidget(HookBuilder(builder: builder(stream))); + expect(value.data, 42); + }); + testWidgets('If preserveState == false, changing stream resets value', + (tester) async { + AsyncSnapshot value; + Widget Function(BuildContext) builder(Stream stream) { + return (context) { + value = useStream(stream, preserveState: false); + return Container(); + }; + } + + var stream = Stream.fromFuture(Future.value(0)); + await tester.pumpWidget(HookBuilder(builder: builder(stream))); + expect(value.data, null); + await tester.pumpWidget(HookBuilder(builder: builder(stream))); + expect(value.data, 0); + + stream = Stream.fromFuture(Future.value(42)); + await tester.pumpWidget(HookBuilder(builder: builder(stream))); + expect(value.data, null); + await tester.pumpWidget(HookBuilder(builder: builder(stream))); + expect(value.data, 42); + }); + + + Widget Function(BuildContext) snapshotText(Stream stream, {String initialData}) { return (context) { From 598d2f915b717a3f3e784443fedd42f281231b99 Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Fri, 1 Feb 2019 20:03:48 +0100 Subject: [PATCH 051/384] new: added reassemble life-cycle for HookState (#47) new: added reassemble life-cycle for HookState closes #44 --- .travis.yml | 2 +- CHANGELOG.md | 1 + lib/src/framework.dart | 36 ++++++++++++++++++----------- pubspec.lock | 8 +++---- test/hook_widget_test.dart | 46 ++++++++++++++++++++++++++++++++++++++ test/mock.dart | 10 +++++++++ 6 files changed, 85 insertions(+), 18 deletions(-) diff --git a/.travis.yml b/.travis.yml index e823856a..22230988 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,7 +4,7 @@ os: sudo: false before_script: - cd .. - - git clone https://github.com/flutter/flutter.git -b beta + - git clone https://github.com/flutter/flutter.git -b master - export PATH=$PATH:$PWD/flutter/bin - export PATH=$PATH:$PWD/flutter/bin/cache/dart-sdk/bin - flutter doctor diff --git a/CHANGELOG.md b/CHANGELOG.md index 6733cffc..aec2722e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ ## 0.3.0: +- NEW: new `reassemble` life-cycle on `HookState`. It is equivalent to `State.ressemble` of statefulwidgets. - NEW: `useStream` and `useFuture` now have an optional `preserveState` flag. This toggle how these hooks behaves when changing the stream/future: If true (default) they keep the previous value, else they reset to initialState. diff --git a/lib/src/framework.dart b/lib/src/framework.dart index b90b1c90..94b6d37f 100644 --- a/lib/src/framework.dart +++ b/lib/src/framework.dart @@ -198,12 +198,10 @@ abstract class HookState> { /// Equivalent of [State.initState] for [HookState] @protected - @mustCallSuper void initHook() {} /// Equivalent of [State.dispose] for [HookState] @protected - @mustCallSuper void dispose() {} /// Called everytimes the [HookState] is requested @@ -214,8 +212,18 @@ abstract class HookState> { /// Equivalent of [State.didUpdateWidget] for [HookState] @protected - @mustCallSuper - void didUpdateHook(covariant Hook oldHook) {} + void didUpdateHook(T oldHook) {} + + /// {@macro flutter.widgets.reassemble} + /// + /// In addition to this method being invoked, it is guaranteed that the + /// [build] method will be invoked when a reassemble is signaled. Most + /// widgets therefore do not need to do anything in the [reassemble] method. + /// + /// See also: + /// + /// * [State.reassemble] + void reassemble() {} /// Equivalent of [State.setState] for [HookState] @protected @@ -303,6 +311,17 @@ This may happen if the call to `Hook.use` is made under some condition. } } + @override + void reassemble() { + super.reassemble(); + _didReassemble = true; + if (_hooks != null) { + for (final hook in _hooks) { + hook.reassemble(); + } + } + } + R _use(Hook hook) { assert(_debugIsBuilding == true, ''' Hooks should only be called within the build method of a widget. @@ -419,15 +438,6 @@ abstract class HookWidget extends StatefulWidget { } class _HookWidgetState extends State { - @override - void reassemble() { - super.reassemble(); - assert(() { - (context as HookElement)._didReassemble = true; - return true; - }()); - } - @override Widget build(BuildContext context) { return widget.build(context); diff --git a/pubspec.lock b/pubspec.lock index ca8ec7c4..08cbecef 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -92,7 +92,7 @@ packages: name: source_span url: "https://pub.dartlang.org" source: hosted - version: "1.4.1" + version: "1.5.4" stack_trace: dependency: transitive description: @@ -120,14 +120,14 @@ packages: name: term_glyph url: "https://pub.dartlang.org" source: hosted - version: "1.0.1" + version: "1.1.0" test_api: dependency: transitive description: name: test_api url: "https://pub.dartlang.org" source: hosted - version: "0.2.1" + version: "0.2.2" typed_data: dependency: transitive description: @@ -143,4 +143,4 @@ packages: source: hosted version: "2.0.8" sdks: - dart: ">=2.0.0 <3.0.0" + dart: ">=2.1.0 <3.0.0" diff --git a/test/hook_widget_test.dart b/test/hook_widget_test.dart index 575f6ad6..84861686 100644 --- a/test/hook_widget_test.dart +++ b/test/hook_widget_test.dart @@ -10,12 +10,14 @@ void main() { final dispose = Func0(); final initHook = Func0(); final didUpdateHook = Func1(); + final reassemble = Func0(); final builder = Func1(); final createHook = () => HookTest( build: build.call, dispose: dispose.call, didUpdateHook: didUpdateHook.call, + reassemble: reassemble.call, initHook: initHook.call, ); @@ -25,6 +27,7 @@ void main() { reset(dispose); reset(initHook); reset(didUpdateHook); + reset(reassemble); }); testWidgets('hooks can be disposed independently with keys', (tester) async { @@ -390,6 +393,49 @@ void main() { verifyNever(dispose.call()); }); + testWidgets('hot-reload calls reassemble', (tester) async { + final reassemble2 = Func0(); + final didUpdateHook2 = Func1>(); + await tester.pumpWidget(HookBuilder(builder: (context) { + Hook.use(createHook()); + Hook.use(HookTest( + reassemble: reassemble2, didUpdateHook: didUpdateHook2)); + return Container(); + })); + + verifyNoMoreInteractions(reassemble); + + hotReload(tester); + await tester.pump(); + + verifyInOrder([ + reassemble.call(), + reassemble2.call(), + didUpdateHook.call(any), + didUpdateHook2.call(any), + ]); + verifyNoMoreInteractions(reassemble); + }); + + testWidgets("hot-reload don't reassemble newly added hooks", (tester) async { + await tester.pumpWidget(HookBuilder(builder: (context) { + Hook.use(HookTest()); + return Container(); + })); + + verifyNoMoreInteractions(reassemble); + + hotReload(tester); + await tester.pumpWidget(HookBuilder(builder: (context) { + Hook.use(HookTest()); + Hook.use(createHook()); + return Container(); + })); + + verifyNoMoreInteractions(didUpdateHook); + verifyNoMoreInteractions(reassemble); + }); + testWidgets('hot-reload can add hooks at the end of the list', (tester) async { HookTest hook1; diff --git a/test/mock.dart b/test/mock.dart index 2ecdc574..66976d66 100644 --- a/test/mock.dart +++ b/test/mock.dart @@ -30,6 +30,7 @@ class HookTest extends Hook { final void Function() dispose; final void Function() initHook; final void Function(HookTest previousHook) didUpdateHook; + final void Function() reassemble; final HookStateTest Function() createStateFn; HookTest({ @@ -37,6 +38,7 @@ class HookTest extends Hook { this.dispose, this.initHook, this.didUpdateHook, + this.reassemble, this.createStateFn, List keys, }) : super(keys: keys); @@ -71,6 +73,14 @@ class HookStateTest extends HookState> { } } + @override + void reassemble() { + super.reassemble(); + if (hook.reassemble != null) { + hook.reassemble(); + } + } + @override R build(BuildContext context) { if (hook.build != null) { From be20e9b88374acc95b5e37c6238be8e261dc1a5e Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Fri, 1 Feb 2019 20:12:26 +0100 Subject: [PATCH 052/384] bump-version (#51) --- example/pubspec.lock | 2 +- pubspec.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/example/pubspec.lock b/example/pubspec.lock index 26ebf7a2..3db99035 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -40,7 +40,7 @@ packages: path: ".." relative: true source: path - version: "0.2.1" + version: "0.3.0-dev" flutter_test: dependency: "direct dev" description: flutter diff --git a/pubspec.yaml b/pubspec.yaml index 9076e6a7..e476dd8e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -3,7 +3,7 @@ description: A flutter implementation of React hooks. It adds a new kind of widg homepage: https://github.com/rrousselGit/flutter_hooks author: Remi Rousselet -version: 0.2.1 +version: 0.3.0-dev environment: sdk: ">=2.0.0-dev.68.0 <3.0.0" From e1048e1a8db6d6f6c32aa1cd6d0c1a50f9f42ff9 Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Fri, 1 Feb 2019 20:28:00 +0100 Subject: [PATCH 053/384] Add didBuild life-cycle (#46) --- CHANGELOG.md | 2 + example/lib/use_stream.dart | 2 + example/pubspec.lock | 2 +- example/pubspec.yaml | 2 +- lib/src/framework.dart | 28 +++++- test/hook_widget_test.dart | 169 +++++++++++++++++++++++++----------- test/mock.dart | 10 +++ test/use_stream_test.dart | 2 - 8 files changed, 162 insertions(+), 55 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index aec2722e..2050a6f9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,7 @@ ## 0.3.0: +- NEW: new life-cycle availble on `HookState`: `didBuild`. +This life-cycle is called synchronously right after `build` method of `HookWidget` finished. - NEW: new `reassemble` life-cycle on `HookState`. It is equivalent to `State.ressemble` of statefulwidgets. - NEW: `useStream` and `useFuture` now have an optional `preserveState` flag. This toggle how these hooks behaves when changing the stream/future: diff --git a/example/lib/use_stream.dart b/example/lib/use_stream.dart index 87e242d5..b54fb121 100644 --- a/example/lib/use_stream.dart +++ b/example/lib/use_stream.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; diff --git a/example/pubspec.lock b/example/pubspec.lock index 3db99035..ac530947 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -99,7 +99,7 @@ packages: name: source_span url: "https://pub.dartlang.org" source: hosted - version: "1.5.4" + version: "1.5.3" stack_trace: dependency: transitive description: diff --git a/example/pubspec.yaml b/example/pubspec.yaml index c66951d2..8698db64 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -4,7 +4,7 @@ description: A new Flutter project. version: 1.0.0+1 environment: - sdk: ">=2.1.0 <3.0.0" + sdk: ">=2.0.0-dev.68.0 <3.0.0" dependencies: flutter: diff --git a/lib/src/framework.dart b/lib/src/framework.dart index 94b6d37f..adbcff74 100644 --- a/lib/src/framework.dart +++ b/lib/src/framework.dart @@ -204,6 +204,10 @@ abstract class HookState> { @protected void dispose() {} + /// Called synchronously after the [HookWidget.build] method finished + @protected + void didBuild() {} + /// Called everytimes the [HookState] is requested /// /// [build] is where an [HookState] may use other hooks. This restriction is made to ensure that hooks are unconditionally always requested @@ -253,7 +257,7 @@ class HookElement extends StatefulElement { HookWidget get widget => super.widget as HookWidget; @override - void performRebuild() { + Widget build() { _currentHook = _hooks?.iterator; // first iterator always has null _currentHook?.moveNext(); @@ -266,7 +270,7 @@ class HookElement extends StatefulElement { return true; }()); HookElement._currentContext = this; - super.performRebuild(); + final result = super.build(); HookElement._currentContext = null; // dispose removed items @@ -290,6 +294,26 @@ This may happen if the call to `Hook.use` is made under some condition. _debugIsBuilding = false; return true; }()); + return result; + } + + @override + Element updateChild(Element child, Widget newWidget, dynamic newSlot) { + if (_hooks != null) { + for (final hook in _hooks.reversed) { + try { + hook.didBuild(); + } catch (exception, stack) { + FlutterError.reportError(FlutterErrorDetails( + exception: exception, + stack: stack, + library: 'hooks library', + context: 'while calling `didBuild` on ${hook.runtimeType}', + )); + } + } + } + return super.updateChild(child, newWidget, newSlot); } @override diff --git a/test/hook_widget_test.dart b/test/hook_widget_test.dart index 84861686..799f6a88 100644 --- a/test/hook_widget_test.dart +++ b/test/hook_widget_test.dart @@ -10,6 +10,7 @@ void main() { final dispose = Func0(); final initHook = Func0(); final didUpdateHook = Func1(); + final didBuild = Func0(); final reassemble = Func0(); final builder = Func1(); @@ -19,11 +20,21 @@ void main() { didUpdateHook: didUpdateHook.call, reassemble: reassemble.call, initHook: initHook.call, + didBuild: didBuild, ); + void verifyNoMoreHookInteration() { + verifyNoMoreInteractions(build); + verifyNoMoreInteractions(didBuild); + verifyNoMoreInteractions(dispose); + verifyNoMoreInteractions(initHook); + verifyNoMoreInteractions(didUpdateHook); + } + tearDown(() { reset(builder); reset(build); + reset(didBuild); reset(dispose); reset(initHook); reset(didUpdateHook); @@ -70,6 +81,7 @@ void main() { didUpdateHook: didUpdateHook.call, initHook: initHook.call, keys: keys, + didBuild: didBuild, createStateFn: createState.call, )); return Container(); @@ -82,24 +94,18 @@ void main() { createState.call(), initHook.call(), build.call(context), + didBuild.call(), ]); - verifyNoMoreInteractions(createState); - verifyNoMoreInteractions(initHook); - verifyNoMoreInteractions(build); - verifyNoMoreInteractions(didUpdateHook); - verifyNoMoreInteractions(dispose); + verifyNoMoreHookInteration(); await tester.pumpWidget(HookBuilder(builder: builder.call)); verifyInOrder([ didUpdateHook.call(any), build.call(context), + didBuild.call(), ]); - verifyNoMoreInteractions(createState); - verifyNoMoreInteractions(initHook); - verifyNoMoreInteractions(build); - verifyNoMoreInteractions(didUpdateHook); - verifyNoMoreInteractions(dispose); + verifyNoMoreHookInteration(); // from null to array keys = []; @@ -109,13 +115,10 @@ void main() { dispose.call(), createState.call(), initHook.call(), - build.call(context) + build.call(context), + didBuild.call(), ]); - verifyNoMoreInteractions(createState); - verifyNoMoreInteractions(initHook); - verifyNoMoreInteractions(build); - verifyNoMoreInteractions(didUpdateHook); - verifyNoMoreInteractions(dispose); + verifyNoMoreHookInteration(); // array immutable keys.add(42); @@ -125,12 +128,9 @@ void main() { verifyInOrder([ didUpdateHook.call(any), build.call(context), + didBuild.call(), ]); - verifyNoMoreInteractions(createState); - verifyNoMoreInteractions(initHook); - verifyNoMoreInteractions(build); - verifyNoMoreInteractions(didUpdateHook); - verifyNoMoreInteractions(dispose); + verifyNoMoreHookInteration(); // new array but content equal keys = [42]; @@ -140,12 +140,9 @@ void main() { verifyInOrder([ didUpdateHook.call(any), build.call(context), + didBuild.call(), ]); - verifyNoMoreInteractions(createState); - verifyNoMoreInteractions(initHook); - verifyNoMoreInteractions(build); - verifyNoMoreInteractions(didUpdateHook); - verifyNoMoreInteractions(dispose); + verifyNoMoreHookInteration(); // new array new content keys = [44]; @@ -156,13 +153,10 @@ void main() { dispose.call(), createState.call(), initHook.call(), - build.call(context) + build.call(context), + didBuild.call() ]); - verifyNoMoreInteractions(createState); - verifyNoMoreInteractions(initHook); - verifyNoMoreInteractions(build); - verifyNoMoreInteractions(didUpdateHook); - verifyNoMoreInteractions(dispose); + verifyNoMoreHookInteration(); }); testWidgets('hook & setState', (tester) async { @@ -189,6 +183,92 @@ void main() { expect(hookContext.dirty, true); }); + testWidgets( + 'didBuild when build crash called after FlutterError.onError report', + (tester) async { + final onError = FlutterError.onError; + FlutterError.onError = Func1(); + final errorBuilder = ErrorWidget.builder; + ErrorWidget.builder = Func1(); + when(ErrorWidget.builder(any)).thenReturn(Container()); + try { + when(build.call(any)).thenThrow(42); + when(builder.call(any)).thenAnswer((invocation) { + Hook.use(createHook()); + return Container(); + }); + + await tester.pumpWidget(HookBuilder( + builder: builder.call, + )); + tester.takeException(); + + verifyInOrder([ + build.call(any), + FlutterError.onError(any), + ErrorWidget.builder(any), + didBuild(), + ]); + } finally { + FlutterError.onError = onError; + ErrorWidget.builder = errorBuilder; + } + }); + + testWidgets('didBuild called even if build crashed', (tester) async { + when(build.call(any)).thenThrow(42); + when(builder.call(any)).thenAnswer((invocation) { + Hook.use(createHook()); + return Container(); + }); + + await tester.pumpWidget(HookBuilder( + builder: builder.call, + )); + expect(tester.takeException(), 42); + + verify(didBuild.call()).called(1); + }); + testWidgets('all didBuild called even if one crashes', (tester) async { + final didBuild2 = Func0(); + + when(didBuild.call()).thenThrow(42); + when(builder.call(any)).thenAnswer((invocation) { + Hook.use(createHook()); + Hook.use(HookTest(didBuild: didBuild2)); + return Container(); + }); + + await expectPump( + () => tester.pumpWidget(HookBuilder( + builder: builder.call, + )), + throwsA(42), + ); + + verifyInOrder([ + didBuild2.call(), + didBuild.call(), + ]); + }); + + testWidgets('calls didBuild before building children', (tester) async { + final buildChild = Func1(); + when(buildChild.call(any)).thenReturn(Container()); + + await tester.pumpWidget(HookBuilder( + builder: (context) { + Hook.use(createHook()); + return Builder(builder: buildChild); + }, + )); + + verifyInOrder([ + didBuild(), + buildChild.call(any), + ]); + }); + testWidgets('life-cycles in order', (tester) async { int result; HookTest hook; @@ -209,9 +289,9 @@ void main() { verifyInOrder([ initHook.call(), build.call(context), + didBuild.call(), ]); - verifyZeroInteractions(didUpdateHook); - verifyZeroInteractions(dispose); + verifyNoMoreHookInteration(); when(build.call(context)).thenReturn(24); var previousHook = hook; @@ -221,28 +301,19 @@ void main() { )); expect(result, 24); - verifyInOrder([ - didUpdateHook.call(previousHook), - build.call(any), - ]); - verifyNoMoreInteractions(initHook); - verifyZeroInteractions(dispose); + verifyInOrder( + [didUpdateHook.call(previousHook), build.call(any), didBuild.call()]); + verifyNoMoreHookInteration(); previousHook = hook; await tester.pump(); - verifyNoMoreInteractions(initHook); - verifyNoMoreInteractions(didUpdateHook); - verifyNoMoreInteractions(build); - verifyZeroInteractions(dispose); + verifyNoMoreHookInteration(); await tester.pumpWidget(const SizedBox()); - verifyNoMoreInteractions(initHook); - verifyNoMoreInteractions(didUpdateHook); - verifyNoMoreInteractions(build); - verify(dispose.call()); - verifyNoMoreInteractions(dispose); + verify(dispose.call()).called(1); + verifyNoMoreHookInteration(); }); testWidgets('dispose all called even on failed', (tester) async { diff --git a/test/mock.dart b/test/mock.dart index 66976d66..be7ec820 100644 --- a/test/mock.dart +++ b/test/mock.dart @@ -28,6 +28,7 @@ class Func2 extends Mock implements _Func2 {} class HookTest extends Hook { final R Function(BuildContext context) build; final void Function() dispose; + final void Function() didBuild; final void Function() initHook; final void Function(HookTest previousHook) didUpdateHook; final void Function() reassemble; @@ -40,6 +41,7 @@ class HookTest extends Hook { this.didUpdateHook, this.reassemble, this.createStateFn, + this.didBuild, List keys, }) : super(keys: keys); @@ -73,6 +75,14 @@ class HookStateTest extends HookState> { } } + @override + void didBuild() { + super.didBuild(); + if (hook.didBuild != null) { + hook.didBuild(); + } + } + @override void reassemble() { super.reassemble(); diff --git a/test/use_stream_test.dart b/test/use_stream_test.dart index ee677b0d..e48cc827 100644 --- a/test/use_stream_test.dart +++ b/test/use_stream_test.dart @@ -53,8 +53,6 @@ void main() { expect(value.data, 42); }); - - Widget Function(BuildContext) snapshotText(Stream stream, {String initialData}) { return (context) { From d810b0b75bab8122d5210a8e099ea912278ceabc Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Fri, 1 Feb 2019 20:30:08 +0100 Subject: [PATCH 054/384] bumb-version --- example/pubspec.lock | 4 ++-- pubspec.yaml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/example/pubspec.lock b/example/pubspec.lock index ac530947..145861e5 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -40,7 +40,7 @@ packages: path: ".." relative: true source: path - version: "0.3.0-dev" + version: "0.3.0-dev.1" flutter_test: dependency: "direct dev" description: flutter @@ -99,7 +99,7 @@ packages: name: source_span url: "https://pub.dartlang.org" source: hosted - version: "1.5.3" + version: "1.5.4" stack_trace: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index e476dd8e..1e4fb804 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -3,7 +3,7 @@ description: A flutter implementation of React hooks. It adds a new kind of widg homepage: https://github.com/rrousselGit/flutter_hooks author: Remi Rousselet -version: 0.3.0-dev +version: 0.3.0-dev.1 environment: sdk: ">=2.0.0-dev.68.0 <3.0.0" From 578b1f2c2eaf1d5a6d7a3c9b35b2418069a72bcd Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Mon, 4 Feb 2019 00:15:32 +0100 Subject: [PATCH 055/384] NEW: Better exceptions handling (#53) If a widget throws on the first build or after a hot-reload Next rebuilds can still add/edit hooks until one `build` finishes entirely. closes #52 --- CHANGELOG.md | 1 + lib/src/framework.dart | 97 ++++++++++++++++++++++---------------- lib/src/hooks.dart | 8 ++-- test/hook_widget_test.dart | 93 +++++++++++++++++++++++++++++++++++- 4 files changed, 155 insertions(+), 44 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2050a6f9..c1fbb4bf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ ## 0.3.0: +- NEW: If a widget throws on the first build or after a hot-reload, next rebuilds can still add/edit hooks until one `build` finishes entirely. - NEW: new life-cycle availble on `HookState`: `didBuild`. This life-cycle is called synchronously right after `build` method of `HookWidget` finished. - NEW: new `reassemble` life-cycle on `HookState`. It is equivalent to `State.ressemble` of statefulwidgets. diff --git a/lib/src/framework.dart b/lib/src/framework.dart index adbcff74..544d7e2a 100644 --- a/lib/src/framework.dart +++ b/lib/src/framework.dart @@ -248,7 +248,7 @@ class HookElement extends StatefulElement { bool _debugIsBuilding; bool _didReassemble; - bool _isFirstBuild; + bool _didNotFinishBuildOnce; bool _debugShouldDispose; static HookElement _currentContext; @@ -264,7 +264,7 @@ class HookElement extends StatefulElement { _hookIndex = 0; assert(() { _debugShouldDispose = false; - _isFirstBuild ??= true; + _didNotFinishBuildOnce ??= true; _didReassemble ??= false; _debugIsBuilding = true; return true; @@ -289,7 +289,7 @@ This may happen if the call to `Hook.use` is made under some condition. '''); assert(() { - _isFirstBuild = false; + _didNotFinishBuildOnce = false; _didReassemble = false; _debugIsBuilding = false; return true; @@ -355,7 +355,7 @@ This may happen if the call to `Hook.use` is made under some condition. HookState> hookState; // first build if (_currentHook == null) { - assert(_didReassemble || _isFirstBuild); + assert(_didReassemble || _didNotFinishBuildOnce); hookState = _createHookState(hook); _hooks ??= []; _hooks.add(hookState); @@ -376,56 +376,73 @@ This may happen if the call to `Hook.use` is made under some condition. if (_currentHook.current != null) { _hooks.remove(_currentHook.current..dispose()); // has to be done after the dispose call - hookState = _createHookState(hook); - _hooks.insert(_hookIndex, hookState); - - // we move the iterator back to where it was - _currentHook = _hooks.iterator..moveNext(); - for (var i = 0; i < _hooks.length && _hooks[i] != hookState; i++) { - _currentHook.moveNext(); - } + hookState = _insertHookAt(_hookIndex, hook); } else { - // new hooks have been pushed at the end of the list. - hookState = _createHookState(hook); - _hooks.add(hookState); - - // we put the iterator on added item - _currentHook = _hooks.iterator; - while (_currentHook.current != hookState) { - _currentHook.moveNext(); - } + hookState = _pushHook(hook); } return true; }()); - assert(_currentHook.current?.hook?.runtimeType == hook.runtimeType); - - if (_currentHook.current.hook == hook) { - hookState = _currentHook.current as HookState>; - _currentHook.moveNext(); - } else if (Hook.shouldPreserveState(_currentHook.current.hook, hook)) { - hookState = _currentHook.current as HookState>; - _currentHook.moveNext(); - final previousHook = hookState._hook; - hookState - .._hook = hook - ..didUpdateHook(previousHook); + if (_didNotFinishBuildOnce && _currentHook.current == null) { + hookState = _pushHook(hook); } else { - _hooks.removeAt(_hookIndex).dispose(); - hookState = _createHookState(hook); - _hooks.insert(_hookIndex, hookState); + assert(_currentHook.current != null); + assert(_debugTypesAreRight(hook)); - // we move the iterator back to where it was - _currentHook = _hooks.iterator..moveNext(); - for (var i = 0; i < _hooks.length && _hooks[i] != hookState; i++) { + if (_currentHook.current.hook == hook) { + hookState = _currentHook.current as HookState>; + _currentHook.moveNext(); + } else if (Hook.shouldPreserveState(_currentHook.current.hook, hook)) { + hookState = _currentHook.current as HookState>; + _currentHook.moveNext(); + final previousHook = hookState._hook; + hookState + .._hook = hook + ..didUpdateHook(previousHook); + } else { + hookState = _replaceHookAt(_hookIndex, hook); + _resetsIterator(hookState); _currentHook.moveNext(); } - _currentHook.moveNext(); } } _hookIndex++; return hookState.build(this); } + HookState> _replaceHookAt(int index, Hook hook) { + _hooks.removeAt(_hookIndex).dispose(); + var hookState = _createHookState(hook); + _hooks.insert(_hookIndex, hookState); + return hookState; + } + + HookState> _insertHookAt(int index, Hook hook) { + var hookState = _createHookState(hook); + _hooks.insert(index, hookState); + _resetsIterator(hookState); + return hookState; + } + + HookState> _pushHook(Hook hook) { + var hookState = _createHookState(hook); + _hooks.add(hookState); + _resetsIterator(hookState); + return hookState; + } + + bool _debugTypesAreRight(Hook hook) { + assert(_currentHook.current.hook.runtimeType == hook.runtimeType); + return true; + } + + /// we put the iterator on added item + void _resetsIterator(HookState hookState) { + _currentHook = _hooks.iterator; + while (_currentHook.current != hookState) { + _currentHook.moveNext(); + } + } + HookState> _createHookState(Hook hook) { return hook.createState() .._element = state diff --git a/lib/src/hooks.dart b/lib/src/hooks.dart index 0f7b406a..cee16b88 100644 --- a/lib/src/hooks.dart +++ b/lib/src/hooks.dart @@ -677,17 +677,19 @@ class _StreamHookState extends HookState, _StreamHook> { current.inState(ConnectionState.none); } +typedef Dispose = void Function(); + /// A hook for side-effects /// /// [useEffect] is called synchronously on every [HookWidget.build], unless /// [keys] is specified. In which case [useEffect] is called again only if /// any value inside [keys] as changed. -void useEffect(VoidCallback Function() effect, [List keys]) { +void useEffect(Dispose Function() effect, [List keys]) { Hook.use(_EffectHook(effect, keys)); } class _EffectHook extends Hook { - final VoidCallback Function() effect; + final Dispose Function() effect; const _EffectHook(this.effect, [List keys]) : assert(effect != null), @@ -698,7 +700,7 @@ class _EffectHook extends Hook { } class _EffectHookState extends HookState { - VoidCallback disposer; + Dispose disposer; @override void initHook() { diff --git a/test/hook_widget_test.dart b/test/hook_widget_test.dart index 799f6a88..6396cb43 100644 --- a/test/hook_widget_test.dart +++ b/test/hook_widget_test.dart @@ -1,4 +1,4 @@ -// ignore_for_file: invalid_use_of_protected_member +// ignore_for_file: invalid_use_of_protected_member, only_throw_errors import 'package:flutter/widgets.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; @@ -41,6 +41,97 @@ void main() { reset(reassemble); }); + testWidgets( + 'until one build finishes without crashing, it is possible to add hooks', + (tester) async { + await tester.pumpWidget(HookBuilder(builder: (_) { + throw 0; + })); + expect(tester.takeException(), 0); + + await tester.pumpWidget(HookBuilder(builder: (_) { + Hook.use(HookTest()); + throw 1; + })); + expect(tester.takeException(), 1); + + await tester.pumpWidget(HookBuilder(builder: (_) { + Hook.use(HookTest()); + Hook.use(HookTest()); + throw 2; + })); + expect(tester.takeException(), 2); + + await tester.pumpWidget(HookBuilder(builder: (_) { + Hook.use(HookTest()); + Hook.use(HookTest()); + Hook.use(HookTest()); + return Container(); + })); + }); + + testWidgets( + 'After a build suceeded, expections do not allow adding more hooks', + (tester) async { + await tester.pumpWidget(HookBuilder(builder: (_) { + return Container(); + })); + + await tester.pumpWidget(HookBuilder(builder: (_) { + throw 1; + })); + expect(tester.takeException(), 1); + + await tester.pumpWidget(HookBuilder(builder: (_) { + Hook.use(HookTest()); + return Container(); + })); + expect(tester.takeException(), isAssertionError); + }); + + testWidgets( + "After hot-reload that throws it's still possible to add hooks until one build suceed", + (tester) async { + await tester.pumpWidget(HookBuilder(builder: (_) { + return Container(); + })); + + hotReload(tester); + + await tester.pumpWidget(HookBuilder(builder: (_) { + throw 0; + })); + expect(tester.takeException(), 0); + + await tester.pumpWidget(HookBuilder(builder: (_) { + Hook.use(HookTest()); + return Container(); + })); + }); + + testWidgets( + 'After hot-reload that throws, hooks are correctly disposed when build suceeeds with less hooks', + (tester) async { + await tester.pumpWidget(HookBuilder(builder: (_) { + Hook.use(createHook()); + return Container(); + })); + + hotReload(tester); + + await tester.pumpWidget(HookBuilder(builder: (_) { + throw 0; + })); + expect(tester.takeException(), 0); + verifyNever(dispose()); + + await tester.pumpWidget(HookBuilder(builder: (_) { + return Container(); + })); + + verify(dispose()).called(1); + }); + testWidgets('hooks can be disposed independently with keys', (tester) async { List keys; List keys2; From ce8ef73487bd21d6c573fb7431886dc55a00a3e9 Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Mon, 4 Feb 2019 00:16:53 +0100 Subject: [PATCH 056/384] bump-version (#54) --- example/pubspec.lock | 2 +- pubspec.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/example/pubspec.lock b/example/pubspec.lock index 145861e5..635c30b0 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -40,7 +40,7 @@ packages: path: ".." relative: true source: path - version: "0.3.0-dev.1" + version: "0.3.0-dev.2" flutter_test: dependency: "direct dev" description: flutter diff --git a/pubspec.yaml b/pubspec.yaml index 1e4fb804..7cdd48bf 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -3,7 +3,7 @@ description: A flutter implementation of React hooks. It adds a new kind of widg homepage: https://github.com/rrousselGit/flutter_hooks author: Remi Rousselet -version: 0.3.0-dev.1 +version: 0.3.0-dev.2 environment: sdk: ">=2.0.0-dev.68.0 <3.0.0" From d016c8a037488bfc47a240e956d5c936c70c113d Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Mon, 4 Feb 2019 13:43:10 +0100 Subject: [PATCH 057/384] NEW: hooks are visible on HookElement (#55) --- CHANGELOG.md | 1 + lib/src/framework.dart | 8 ++++++++ test/hook_widget_test.dart | 18 +++++++++++++++++- 3 files changed, 26 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c1fbb4bf..7ca04970 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ ## 0.3.0: +- NEW: Hooks are now visible on `HookElement` through `debugHooks` in development, for testing purposes. - NEW: If a widget throws on the first build or after a hot-reload, next rebuilds can still add/edit hooks until one `build` finishes entirely. - NEW: new life-cycle availble on `HookState`: `didBuild`. This life-cycle is called synchronously right after `build` method of `HookWidget` finished. diff --git a/lib/src/framework.dart b/lib/src/framework.dart index 544d7e2a..3fe056c7 100644 --- a/lib/src/framework.dart +++ b/lib/src/framework.dart @@ -297,6 +297,14 @@ This may happen if the call to `Hook.use` is made under some condition. return result; } + /// A read-only list of all hooks available. + /// + /// These should not be used directly and are exposed + @visibleForTesting + List get debugHooks { + return List.unmodifiable(_hooks); + } + @override Element updateChild(Element child, Widget newWidget, dynamic newSlot) { if (_hooks != null) { diff --git a/test/hook_widget_test.dart b/test/hook_widget_test.dart index 6396cb43..733e60e5 100644 --- a/test/hook_widget_test.dart +++ b/test/hook_widget_test.dart @@ -1,5 +1,4 @@ // ignore_for_file: invalid_use_of_protected_member, only_throw_errors - import 'package:flutter/widgets.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; @@ -41,6 +40,23 @@ void main() { reset(reassemble); }); + testWidgets( + 'until one build finishes without crashing, it is possible to add hooks', + (tester) async { + await tester.pumpWidget(HookBuilder(builder: (_) { + Hook.use(HookTest()); + Hook.use(HookTest()); + return Container(); + })); + + final element = tester.element(find.byType(HookBuilder)) as HookElement; + + expect(element.debugHooks.length, 2); + expect(element.debugHooks.first, isInstanceOf>()); + expect(element.debugHooks.last, isInstanceOf>()); + expect(() => element.debugHooks[0] = null, throwsUnsupportedError); + expect(() => element.debugHooks.add(null), throwsUnsupportedError); + }); testWidgets( 'until one build finishes without crashing, it is possible to add hooks', (tester) async { From bbcc2ece8b0238e2369c5cfe1d477f2eca8f2e49 Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Mon, 4 Feb 2019 13:52:47 +0100 Subject: [PATCH 058/384] bump-version (#56) --- example/pubspec.lock | 2 +- pubspec.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/example/pubspec.lock b/example/pubspec.lock index 635c30b0..db6a87fb 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -40,7 +40,7 @@ packages: path: ".." relative: true source: path - version: "0.3.0-dev.2" + version: "0.3.0-dev.3" flutter_test: dependency: "direct dev" description: flutter diff --git a/pubspec.yaml b/pubspec.yaml index 7cdd48bf..adaace20 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -3,7 +3,7 @@ description: A flutter implementation of React hooks. It adds a new kind of widg homepage: https://github.com/rrousselGit/flutter_hooks author: Remi Rousselet -version: 0.3.0-dev.2 +version: 0.3.0-dev.3 environment: sdk: ">=2.0.0-dev.68.0 <3.0.0" From 71457947e7339aa48879d5e58367362f46adc58d Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Mon, 18 Feb 2019 23:41:43 +0100 Subject: [PATCH 059/384] fixes an issue where adding more than one hook after an exception failed (#58) --- analysis_options.yaml | 1 - lib/src/framework.dart | 1 + test/hook_widget_test.dart | 30 +++++++++++++++++++++++++----- 3 files changed, 26 insertions(+), 6 deletions(-) diff --git a/analysis_options.yaml b/analysis_options.yaml index 7579a070..cd7a9506 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -63,7 +63,6 @@ linter: - prefer_typing_uninitialized_variables - recursive_getters - slash_for_doc_comments - - super_goes_last - test_types_in_equals - throw_in_finally - type_init_formals diff --git a/lib/src/framework.dart b/lib/src/framework.dart index 3fe056c7..b368925c 100644 --- a/lib/src/framework.dart +++ b/lib/src/framework.dart @@ -392,6 +392,7 @@ This may happen if the call to `Hook.use` is made under some condition. }()); if (_didNotFinishBuildOnce && _currentHook.current == null) { hookState = _pushHook(hook); + _currentHook.moveNext(); } else { assert(_currentHook.current != null); assert(_debugTypesAreRight(hook)); diff --git a/test/hook_widget_test.dart b/test/hook_widget_test.dart index 733e60e5..f2833ff9 100644 --- a/test/hook_widget_test.dart +++ b/test/hook_widget_test.dart @@ -40,9 +40,7 @@ void main() { reset(reassemble); }); - testWidgets( - 'until one build finishes without crashing, it is possible to add hooks', - (tester) async { + testWidgets('HookElement exposes an immutable list of hooks', (tester) async { await tester.pumpWidget(HookBuilder(builder: (_) { Hook.use(HookTest()); Hook.use(HookTest()); @@ -73,17 +71,39 @@ void main() { await tester.pumpWidget(HookBuilder(builder: (_) { Hook.use(HookTest()); - Hook.use(HookTest()); + Hook.use(HookTest()); throw 2; })); expect(tester.takeException(), 2); await tester.pumpWidget(HookBuilder(builder: (_) { Hook.use(HookTest()); + Hook.use(HookTest()); + Hook.use(HookTest()); + return Container(); + })); + }); + testWidgets( + 'until one build finishes without crashing, it is possible to add hooks #2', + (tester) async { + await tester.pumpWidget(HookBuilder(builder: (_) { + throw 0; + })); + expect(tester.takeException(), 0); + + await tester.pumpWidget(HookBuilder(builder: (_) { Hook.use(HookTest()); + throw 1; + })); + expect(tester.takeException(), 1); + + await tester.pumpWidget(HookBuilder(builder: (_) { Hook.use(HookTest()); - return Container(); + Hook.use(HookTest()); + Hook.use(HookTest()); + throw 2; })); + expect(tester.takeException(), 2); }); testWidgets( From eb05d8108df8d364a61e823c8bbbaa1d430fae8a Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Tue, 19 Feb 2019 00:39:34 +0100 Subject: [PATCH 060/384] bumb version --- example/pubspec.lock | 2 +- pubspec.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/example/pubspec.lock b/example/pubspec.lock index db6a87fb..f7c87eb3 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -40,7 +40,7 @@ packages: path: ".." relative: true source: path - version: "0.3.0-dev.3" + version: "0.3.0-dev.4" flutter_test: dependency: "direct dev" description: flutter diff --git a/pubspec.yaml b/pubspec.yaml index adaace20..2f5d8652 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -3,7 +3,7 @@ description: A flutter implementation of React hooks. It adds a new kind of widg homepage: https://github.com/rrousselGit/flutter_hooks author: Remi Rousselet -version: 0.3.0-dev.3 +version: 0.3.0-dev.4 environment: sdk: ">=2.0.0-dev.68.0 <3.0.0" From 41f149d37ec7d6afae239662b140088b9023ea1b Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Mon, 4 Mar 2019 22:14:28 +0100 Subject: [PATCH 061/384] use List instead of List for keys This fixes implicit-dynamic mistakenly reporting errors. --- CHANGELOG.md | 5 +++-- example/lib/use_effect.dart | 6 +++--- lib/src/framework.dart | 2 +- lib/src/hooks.dart | 24 ++++++++++----------- test/hook_widget_test.dart | 16 +++++++------- test/memoized_test.dart | 26 +++++++++++------------ test/use_animation_controller_test.dart | 4 ++-- test/use_effect_test.dart | 28 ++++++++++++------------- test/use_stream_controller_test.dart | 2 +- test/use_ticker_provider_test.dart | 4 ++-- test/use_value_notifier_test.dart | 6 +++--- 11 files changed, 62 insertions(+), 61 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7ca04970..2f773a28 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,10 +1,11 @@ ## 0.3.0: +- FIX: use List instead of List for keys. This fixes `implicit-dynamic` rule mistakenly reporting errors. - NEW: Hooks are now visible on `HookElement` through `debugHooks` in development, for testing purposes. - NEW: If a widget throws on the first build or after a hot-reload, next rebuilds can still add/edit hooks until one `build` finishes entirely. - NEW: new life-cycle availble on `HookState`: `didBuild`. -This life-cycle is called synchronously right after `build` method of `HookWidget` finished. -- NEW: new `reassemble` life-cycle on `HookState`. It is equivalent to `State.ressemble` of statefulwidgets. + This life-cycle is called synchronously right after `build` method of `HookWidget` finished. +- NEW: new `reassemble` life-cycle on `HookState`. It is equivalent to `State.ressemble` of statefulwidgets. - NEW: `useStream` and `useFuture` now have an optional `preserveState` flag. This toggle how these hooks behaves when changing the stream/future: If true (default) they keep the previous value, else they reset to initialState. diff --git a/example/lib/use_effect.dart b/example/lib/use_effect.dart index ad76aa36..a9c2951f 100644 --- a/example/lib/use_effect.dart +++ b/example/lib/use_effect.dart @@ -49,7 +49,7 @@ StreamController _useLocalStorageInt( int defaultValue = 0, }) { // Custom hooks can use additional hooks internally! - final controller = useStreamController(keys: [key]); + final controller = useStreamController(keys: [key]); // Pass a callback to the useEffect hook. This function should be called on // first build and every time the controller or key changes @@ -68,7 +68,7 @@ StreamController _useLocalStorageInt( // Pass the controller and key to the useEffect hook. This will ensure the // useEffect hook is only called the first build or when one of the the // values changes. - [controller, key], + [controller, key], ); // Load the initial value from local storage and add it as the initial value @@ -83,7 +83,7 @@ StreamController _useLocalStorageInt( // Pass the controller and key to the useEffect hook. This will ensure the // useEffect hook is only called the first build or when one of the the // values changes. - [controller, key], + [controller, key], ); // Finally, return the StreamController. This allows users to add values from diff --git a/lib/src/framework.dart b/lib/src/framework.dart index b368925c..324d9983 100644 --- a/lib/src/framework.dart +++ b/lib/src/framework.dart @@ -137,7 +137,7 @@ abstract class Hook { /// When a new [Hook] is created, the framework checks if keys matches using [Hook.shouldPreserveState]. /// If they don't, the previously created [HookState] is disposed, and a new one is created /// using [Hook.createState], followed by [HookState.initHook]. - final List keys; + final List keys; /// The algorithm to determine if a [HookState] should be reused or disposed. /// diff --git a/lib/src/hooks.dart b/lib/src/hooks.dart index cee16b88..75ffe134 100644 --- a/lib/src/hooks.dart +++ b/lib/src/hooks.dart @@ -114,7 +114,7 @@ class _ReducerdHookState /// * [keys] can be use to specify a list of objects for [useMemoized] to watch. /// So that whenever [Object.operator==] fails on any parameter or if the length of [keys] changes, /// [valueBuilder] is called again. -T useMemoized(T Function() valueBuilder, [List keys = const []]) { +T useMemoized(T Function() valueBuilder, [List keys = const []]) { return Hook.use(_MemoizedHook( valueBuilder, keys: keys, @@ -131,7 +131,7 @@ BuildContext useContext() { class _MemoizedHook extends Hook { final T Function() valueBuilder; - const _MemoizedHook(this.valueBuilder, {List keys = const []}) + const _MemoizedHook(this.valueBuilder, {List keys = const []}) : assert(valueBuilder != null), assert(keys != null), super(keys: keys); @@ -245,7 +245,7 @@ class _StateHookState extends HookState, _StateHook> { /// /// See also: /// * [SingleTickerProviderStateMixin] -TickerProvider useSingleTickerProvider({List keys}) { +TickerProvider useSingleTickerProvider({List keys}) { return Hook.use( keys != null ? _SingleTickerProviderHook(keys) @@ -254,7 +254,7 @@ TickerProvider useSingleTickerProvider({List keys}) { } class _SingleTickerProviderHook extends Hook { - const _SingleTickerProviderHook([List keys]) : super(keys: keys); + const _SingleTickerProviderHook([List keys]) : super(keys: keys); @override _TickerProviderHookState createState() => _TickerProviderHookState(); @@ -321,7 +321,7 @@ AnimationController useAnimationController({ double upperBound = 1, TickerProvider vsync, AnimationBehavior animationBehavior = AnimationBehavior.normal, - List keys, + List keys, }) { return Hook.use(_AnimationControllerHook( duration: duration, @@ -352,7 +352,7 @@ class _AnimationControllerHook extends Hook { this.upperBound, this.vsync, this.animationBehavior, - List keys, + List keys, }) : super(keys: keys); @override @@ -684,14 +684,14 @@ typedef Dispose = void Function(); /// [useEffect] is called synchronously on every [HookWidget.build], unless /// [keys] is specified. In which case [useEffect] is called again only if /// any value inside [keys] as changed. -void useEffect(Dispose Function() effect, [List keys]) { +void useEffect(Dispose Function() effect, [List keys]) { Hook.use(_EffectHook(effect, keys)); } class _EffectHook extends Hook { final Dispose Function() effect; - const _EffectHook(this.effect, [List keys]) + const _EffectHook(this.effect, [List keys]) : assert(effect != null), super(keys: keys); @@ -745,7 +745,7 @@ StreamController useStreamController( {bool sync = false, VoidCallback onListen, VoidCallback onCancel, - List keys}) { + List keys}) { return Hook.use(_StreamControllerHook( onCancel: onCancel, onListen: onListen, @@ -760,7 +760,7 @@ class _StreamControllerHook extends Hook> { final VoidCallback onCancel; const _StreamControllerHook( - {this.sync = false, this.onListen, this.onCancel, List keys}) + {this.sync = false, this.onListen, this.onCancel, List keys}) : super(keys: keys); @override @@ -813,7 +813,7 @@ class _StreamControllerHookState /// See also: /// * [ValueNotifier] /// * [useValueListenable] -ValueNotifier useValueNotifier([T intialData, List keys]) { +ValueNotifier useValueNotifier([T intialData, List keys]) { return Hook.use(_ValueNotifierHook( initialData: intialData, keys: keys, @@ -823,7 +823,7 @@ ValueNotifier useValueNotifier([T intialData, List keys]) { class _ValueNotifierHook extends Hook> { final T initialData; - const _ValueNotifierHook({List keys, this.initialData}) : super(keys: keys); + const _ValueNotifierHook({List keys, this.initialData}) : super(keys: keys); @override _UseValueNotiferHookState createState() => _UseValueNotiferHookState(); diff --git a/test/hook_widget_test.dart b/test/hook_widget_test.dart index f2833ff9..92f8cfaa 100644 --- a/test/hook_widget_test.dart +++ b/test/hook_widget_test.dart @@ -169,8 +169,8 @@ void main() { }); testWidgets('hooks can be disposed independently with keys', (tester) async { - List keys; - List keys2; + List keys; + List keys2; final dispose2 = Func0(); when(builder.call(any)).thenAnswer((invocation) { @@ -183,20 +183,20 @@ void main() { verifyZeroInteractions(dispose); verifyZeroInteractions(dispose2); - keys = []; + keys = []; await tester.pumpWidget(HookBuilder(builder: builder.call)); verify(dispose.call()).called(1); verifyZeroInteractions(dispose2); - keys2 = []; + keys2 = []; await tester.pumpWidget(HookBuilder(builder: builder.call)); verify(dispose2.call()).called(1); verifyNoMoreInteractions(dispose); }); testWidgets('keys recreate hookstate', (tester) async { - List keys; + List keys; final createState = Func0>(); when(createState.call()).thenReturn(HookStateTest()); @@ -235,7 +235,7 @@ void main() { verifyNoMoreHookInteration(); // from null to array - keys = []; + keys = []; await tester.pumpWidget(HookBuilder(builder: builder.call)); verifyInOrder([ @@ -260,7 +260,7 @@ void main() { verifyNoMoreHookInteration(); // new array but content equal - keys = [42]; + keys = [42]; await tester.pumpWidget(HookBuilder(builder: builder.call)); @@ -272,7 +272,7 @@ void main() { verifyNoMoreHookInteration(); // new array new content - keys = [44]; + keys = [44]; await tester.pumpWidget(HookBuilder(builder: builder.call)); diff --git a/test/memoized_test.dart b/test/memoized_test.dart index 32026c4e..e49d1ce7 100644 --- a/test/memoized_test.dart +++ b/test/memoized_test.dart @@ -5,7 +5,7 @@ import 'mock.dart'; void main() { final builder = Func1(); - final parameterBuilder = Func0(); + final parameterBuilder = Func0>(); final valueBuilder = Func0(); tearDown(() { @@ -16,13 +16,13 @@ void main() { testWidgets('invalid parameters', (tester) async { await tester.pumpWidget(HookBuilder(builder: (context) { - useMemoized(null); + useMemoized(null); return Container(); })); expect(tester.takeException(), isAssertionError); await tester.pumpWidget(HookBuilder(builder: (context) { - useMemoized(() {}, null); + useMemoized(() {}, null); return Container(); })); expect(tester.takeException(), isAssertionError); @@ -61,7 +61,7 @@ void main() { int result; when(valueBuilder.call()).thenReturn(0); - when(parameterBuilder.call()).thenReturn([]); + when(parameterBuilder.call()).thenReturn([]); when(builder.call(any)).thenAnswer((invocation) { result = useMemoized(valueBuilder.call, parameterBuilder.call()); @@ -83,7 +83,7 @@ void main() { /* Add parameter */ - when(parameterBuilder.call()).thenReturn(['foo']); + when(parameterBuilder.call()).thenReturn(['foo']); when(valueBuilder.call()).thenReturn(1); await tester.pumpWidget(HookBuilder(builder: builder.call)); @@ -101,7 +101,7 @@ void main() { /* Remove parameter */ - when(parameterBuilder.call()).thenReturn([]); + when(parameterBuilder.call()).thenReturn([]); when(valueBuilder.call()).thenReturn(2); await tester.pumpWidget(HookBuilder(builder: builder.call)); @@ -133,7 +133,7 @@ void main() { }); when(valueBuilder.call()).thenReturn(0); - when(parameterBuilder.call()).thenReturn(['foo', 42, 24.0]); + when(parameterBuilder.call()).thenReturn(['foo', 42, 24.0]); await tester.pumpWidget(HookBuilder(builder: builder.call)); @@ -143,7 +143,7 @@ void main() { /* Array reference changed but content didn't */ - when(parameterBuilder.call()).thenReturn(['foo', 42, 24.0]); + when(parameterBuilder.call()).thenReturn(['foo', 42, 24.0]); await tester.pumpWidget(HookBuilder(builder: builder.call)); verifyNoMoreInteractions(valueBuilder); @@ -152,7 +152,7 @@ void main() { /* reoder */ when(valueBuilder.call()).thenReturn(1); - when(parameterBuilder.call()).thenReturn([42, 'foo', 24.0]); + when(parameterBuilder.call()).thenReturn([42, 'foo', 24.0]); await tester.pumpWidget(HookBuilder(builder: builder.call)); @@ -161,7 +161,7 @@ void main() { expect(result, 1); when(valueBuilder.call()).thenReturn(2); - when(parameterBuilder.call()).thenReturn([42, 24.0, 'foo']); + when(parameterBuilder.call()).thenReturn([42, 24.0, 'foo']); await tester.pumpWidget(HookBuilder(builder: builder.call)); @@ -172,7 +172,7 @@ void main() { /* value change */ when(valueBuilder.call()).thenReturn(3); - when(parameterBuilder.call()).thenReturn([43, 24.0, 'foo']); + when(parameterBuilder.call()).thenReturn([43, 24.0, 'foo']); await tester.pumpWidget(HookBuilder(builder: builder.call)); @@ -183,7 +183,7 @@ void main() { /* Comparison is done using operator== */ // type change - when(parameterBuilder.call()).thenReturn([43.0, 24.0, 'foo']); + when(parameterBuilder.call()).thenReturn([43.0, 24.0, 'foo']); await tester.pumpWidget(HookBuilder(builder: builder.call)); @@ -201,7 +201,7 @@ void main() { 'memoized parameter reference do not change don\'t call valueBuilder', (tester) async { int result; - final parameters = []; + final parameters = []; when(builder.call(any)).thenAnswer((invocation) { result = useMemoized(valueBuilder.call, parameterBuilder.call()); diff --git a/test/use_animation_controller_test.dart b/test/use_animation_controller_test.dart index 04f77a92..42822df7 100644 --- a/test/use_animation_controller_test.dart +++ b/test/use_animation_controller_test.dart @@ -145,7 +145,7 @@ void main() { }); testWidgets('useAnimationController pass down keys', (tester) async { - List keys; + List keys; AnimationController controller; await tester.pumpWidget(HookBuilder( builder: (context) { @@ -155,7 +155,7 @@ void main() { )); final previous = controller; - keys = []; + keys = []; await tester.pumpWidget(HookBuilder( builder: (context) { diff --git a/test/use_effect_test.dart b/test/use_effect_test.dart index 1ebfdccb..fa556859 100644 --- a/test/use_effect_test.dart +++ b/test/use_effect_test.dart @@ -5,7 +5,7 @@ import 'mock.dart'; final effect = Func0(); final unrelated = Func0(); -List parameters; +List parameters; Widget builder() => HookBuilder(builder: (context) { useEffect(effect.call, parameters); @@ -72,7 +72,7 @@ void main() { ]); verifyNoMoreInteractions(effect); - parameters = ['foo']; + parameters = ['foo']; await tester.pumpWidget(builder()); verifyInOrder([ @@ -83,7 +83,7 @@ void main() { }); testWidgets('useEffect adding parameters call callback', (tester) async { - parameters = ['foo']; + parameters = ['foo']; await tester.pumpWidget(builder()); verifyInOrder([ @@ -92,7 +92,7 @@ void main() { ]); verifyNoMoreInteractions(effect); - parameters = ['foo', 42]; + parameters = ['foo', 42]; await tester.pumpWidget(builder()); verifyInOrder([ @@ -103,7 +103,7 @@ void main() { }); testWidgets('useEffect removing parameters call callback', (tester) async { - parameters = ['foo']; + parameters = ['foo']; await tester.pumpWidget(builder()); verifyInOrder([ @@ -112,7 +112,7 @@ void main() { ]); verifyNoMoreInteractions(effect); - parameters = []; + parameters = []; await tester.pumpWidget(builder()); verifyInOrder([ @@ -122,7 +122,7 @@ void main() { verifyNoMoreInteractions(effect); }); testWidgets('useEffect changing parameters call callback', (tester) async { - parameters = ['foo']; + parameters = ['foo']; await tester.pumpWidget(builder()); verifyInOrder([ @@ -131,7 +131,7 @@ void main() { ]); verifyNoMoreInteractions(effect); - parameters = ['bar']; + parameters = ['bar']; await tester.pumpWidget(builder()); verifyInOrder([ @@ -143,7 +143,7 @@ void main() { testWidgets( 'useEffect with same parameters but different arrays don t call callback', (tester) async { - parameters = ['foo']; + parameters = ['foo']; await tester.pumpWidget(builder()); verifyInOrder([ @@ -152,7 +152,7 @@ void main() { ]); verifyNoMoreInteractions(effect); - parameters = ['foo']; + parameters = ['foo']; await tester.pumpWidget(builder()); verifyNoMoreInteractions(effect); @@ -160,7 +160,7 @@ void main() { testWidgets( 'useEffect with same array but different parameters don t call callback', (tester) async { - parameters = ['foo']; + parameters = ['foo']; await tester.pumpWidget(builder()); verifyInOrder([ @@ -178,14 +178,14 @@ void main() { testWidgets('useEffect disposer called whenever callback called', (tester) async { final effect = Func0(); - List parameters; + List parameters; builder() => HookBuilder(builder: (context) { useEffect(effect.call, parameters); return Container(); }); - parameters = ['foo']; + parameters = ['foo']; final disposerA = Func0(); when(effect.call()).thenReturn(disposerA); @@ -200,7 +200,7 @@ void main() { verifyNoMoreInteractions(effect); verifyZeroInteractions(disposerA); - parameters = ['bar']; + parameters = ['bar']; final disposerB = Func0(); when(effect.call()).thenReturn(disposerB); diff --git a/test/use_stream_controller_test.dart b/test/use_stream_controller_test.dart index 21c06dd4..943d65ca 100644 --- a/test/use_stream_controller_test.dart +++ b/test/use_stream_controller_test.dart @@ -17,7 +17,7 @@ void main() { final previous = controller; await tester.pumpWidget(HookBuilder(builder: (context) { - controller = useStreamController(keys: []); + controller = useStreamController(keys: []); return Container(); })); diff --git a/test/use_ticker_provider_test.dart b/test/use_ticker_provider_test.dart index ae6fe82f..89c41182 100644 --- a/test/use_ticker_provider_test.dart +++ b/test/use_ticker_provider_test.dart @@ -63,7 +63,7 @@ void main() { testWidgets('useSingleTickerProvider pass down keys', (tester) async { TickerProvider provider; - List keys; + List keys; await tester.pumpWidget(HookBuilder(builder: (context) { provider = useSingleTickerProvider(keys: keys); @@ -71,7 +71,7 @@ void main() { })); final previousProvider = provider; - keys = []; + keys = []; await tester.pumpWidget(HookBuilder(builder: (context) { provider = useSingleTickerProvider(keys: keys); diff --git a/test/use_value_notifier_test.dart b/test/use_value_notifier_test.dart index 66a54520..29bba791 100644 --- a/test/use_value_notifier_test.dart +++ b/test/use_value_notifier_test.dart @@ -103,7 +103,7 @@ void main() { await tester.pumpWidget(HookBuilder( builder: (context) { previous = state; - state = useValueNotifier(42, [42]); + state = useValueNotifier(42, [42]); return Container(); }, )); @@ -117,7 +117,7 @@ void main() { await tester.pumpWidget(HookBuilder( builder: (context) { - state = useValueNotifier(null, [42]); + state = useValueNotifier(null, [42]); return Container(); }, )); @@ -125,7 +125,7 @@ void main() { await tester.pumpWidget(HookBuilder( builder: (context) { previous = state; - state = useValueNotifier(42, [42]); + state = useValueNotifier(42, [42]); return Container(); }, )); From e369d64e37783d40e06db8bf148c27c6d976c3e6 Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Fri, 5 Apr 2019 11:42:15 +0200 Subject: [PATCH 062/384] Readme (#64) List hooks in the readme as a table --- README.md | 179 ++------- lib/flutter_hooks.dart | 1 + lib/src/animation.dart | 173 ++++++++ lib/src/async.dart | 275 +++++++++++++ lib/src/framework.dart | 32 +- lib/src/hooks.dart | 863 +--------------------------------------- lib/src/listenable.dart | 107 +++++ lib/src/misc.dart | 86 ++++ lib/src/primitives.dart | 242 +++++++++++ test/mock.dart | 2 +- 10 files changed, 961 insertions(+), 999 deletions(-) create mode 100644 lib/src/animation.dart create mode 100644 lib/src/async.dart create mode 100644 lib/src/listenable.dart create mode 100644 lib/src/misc.dart create mode 100644 lib/src/primitives.dart diff --git a/README.md b/README.md index ea1a23b8..bfcc3fc7 100644 --- a/README.md +++ b/README.md @@ -279,160 +279,55 @@ class _TimeAliveState extends HookState> { ## Existing hooks -Flutter_hooks comes with a list of reusable hooks already provided. They are static methods free to use that includes: +Flutter_hooks comes with a list of reusable hooks already provided. -- useEffect +They are divided in different kinds: -Useful to trigger side effects in a widget and dispose objects. It takes a callback and calls it immediately. That callback may optionally return a function, which will be called when the widget is disposed. +### Primitives -By default, the callback is called on every `build`, but it is possible to override that behavior by passing a list of objects as the second parameter. The callback will then be called only when something inside the list has changed. +A set of low level hooks that interacts with the different life-cycles of a widget -The following call to `useEffect` subscribes to a `Stream` and cancel the subscription when the widget is disposed: +| name | description | +| ----------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------- | +| [useEffect](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useEffect.html) | Useful for side-effects and optionally canceling them. | +| [useState](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useState.html) | Create variable and subscribes to it. | +| [useMemoized](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useMemoized.html) | Cache the instance of a complex object. | +| [useContext](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useContext.html) | Obtain the `BuildContext` of the building `HookWidget`. | +| [useValueChanged](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useValueChanged.html) | Watches a value and calls a callback whenever the value changed. | -```dart -Stream stream; -useEffect(() { - final subscription = stream.listen(print); - // This will cancel the subscription when the widget is disposed - // or if the callback is called again. - return subscription.cancel; - }, - // when the stream change, useEffect will call the callback again. - [stream], -); -``` - -- useState - -Defines + watch a variable and whenever the value change, calls `setState`. - -The following code uses `useState` to make a counter application: - -```dart -class Counter extends HookWidget { - @override - Widget build(BuildContext context) { - final counter = useState(0); - - return GestureDetector( - // automatically triggers a rebuild of Counter widget - onTap: () => counter.value++, - child: Text(counter.value.toString()), - ); - } -} -``` +### Object binding -- useReducer +This category of hooks allows manipulating existing Flutter/Dart objects with hooks. +They will take care of creating/updating/disposing an object. -An alternative to useState for more complex states. +#### dart:async related: -`useReducer` manages an read only state that can be updated by dispatching actions which are interpreted by a `Reducer`. +| name | description | +| ------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------- | +| [useStream](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useStream.html) | Subscribes to a [Stream] and return its current state in an [AsyncSnapshot]. | +| [useStreamController](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useStreamController.html) | Creates a [StreamController] automatically disposed. | +| [useFuture](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useFuture.html) | Subscribes to a [Future] and return its current state in an [AsyncSnapshot]. | -The following makes a counter app with both a "+1" and "-1" button: +#### Animation related: -```dart -class Counter extends HookWidget { - @override - Widget build(BuildContext context) { - final counter = useReducer(_counterReducer, initialState: 0); - - return Column( - children: [ - Text(counter.state.toString()), - IconButton( - icon: const Icon(Icons.add), - onPressed: () => counter.dispatch('increment'), - ), - IconButton( - icon: const Icon(Icons.remove), - onPressed: () => counter.dispatch('decrement'), - ), - ], - ); - } +| name | description | +| --------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------- | +| [useSingleTickerProvider](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useSingleTickerProvider.html) | Creates a single usage [TickerProvider]. | +| [useAnimationController](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useAnimationController.html) | Creates an [AnimationController] automatically disposed. | +| [useAnimation](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useAnimation.html) | Subscribes to an [Animation] and return its value. | - int _counterReducer(int state, String action) { - switch (action) { - case 'increment': - return state + 1; - case 'decrement': - return state - 1; - default: - return state; - } - } -} -``` +#### Listenable related: -- useContext +| name | description | +| ----------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------- | +| [useListenable](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useListenable.html) | Subscribes to a [Listenable] and mark the widget as needing build whenever the listener is called. | +| [useValueNotifier](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useValueNotifier.html) | Creates a [ValueNotifier] automatically disposed. | +| [useValueListenable](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useValueListenable.html) | Subscribes to a [ValueListenable] and return its value. | -Returns the `BuildContext` of the currently building `HookWidget`. This is useful when writing custom hooks that want to manipulate the `BuildContext`. +### Misc -```dart -MyInheritedWidget useMyInheritedWidget() { - BuildContext context = useContext(); - return MyInheritedWidget.of(context); -} -``` +A series of hooks with no particular theme. -- useMemoized - -Takes a callback, calls it synchronously and returns its result. The result is then stored to that subsequent calls will return the same result without calling the callback. - -By default, the callback is called only on the first build. But it is optionally possible to specify a list of objects as the second parameter. The callback will then be called again whenever something inside the list has changed. - -The following sample make an http call and return the created `Future`. And if `userId` changes, a new call will be made: - -```dart -String userId; -final Future response = useMemoized(() { - return http.get('someUrl/$userId'); -}, [userId]); -``` - -- useValueChanged - -Takes a value and a callback, and call the callback whenever the value changed. The callback can optionally return an object, which will be stored and returned as the result of `useValueChanged`. - -The following example implicitly starts a tween animation whenever `color` changes: - -```dart -AnimationController controller; -Color color; - -final colorTween = useValueChanged( - color, - (Color oldColor, Animation oldAnimation) { - return ColorTween( - begin: oldAnimation?.value ?? oldColor, - end: color, - ).animate(controller..forward(from: 0)); - }, - ) ?? - AlwaysStoppedAnimation(color); -``` - -- useAnimationController, useStreamController, useSingleTickerProvider, useValueNotifier - -A set of hooks that handles the whole life-cycle of an object. These hooks will take care of both creating, disposing and updating the object. - -They are the equivalent of both `initState`, `dispose` and `didUpdateWidget` for that specific object. - -```dart -Duration duration; -AnimationController controller = useAnimationController( - // duration is automatically updates when the widget is rebuilt with a different `duration` - duration: duration, -); -``` - -- useStream, useFuture, useAnimation, useValueListenable, useListenable - -A set of hooks that subscribes to an object and calls `setState` accordingly. - -```dart -Stream stream; -// automatically rebuild the widget when a new value is pushed to the stream -AsyncSnapshot snapshot = useStream(stream); -``` +| name | description | +| ------------------------------------------------------------------------------------------------------- | ----------------------------------------------------- | +| [useReducer](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useReducer.html) | An alternative to [useState] for more complex states. | diff --git a/lib/flutter_hooks.dart b/lib/flutter_hooks.dart index 7752ebd5..dbbf2167 100644 --- a/lib/flutter_hooks.dart +++ b/lib/flutter_hooks.dart @@ -1 +1,2 @@ export 'package:flutter_hooks/src/framework.dart'; +export 'package:flutter_hooks/src/hooks.dart'; diff --git a/lib/src/animation.dart b/lib/src/animation.dart new file mode 100644 index 00000000..37ba148d --- /dev/null +++ b/lib/src/animation.dart @@ -0,0 +1,173 @@ +part of 'hooks.dart'; + +/// Subscribes to an [Animation] and return its value. +/// +/// See also: +/// * [Animation] +/// * [useValueListenable], [useListenable], [useStream] +T useAnimation(Animation animation) { + useListenable(animation); + return animation.value; +} + +/// Creates an [AnimationController] automatically disposed. +/// +/// If no [vsync] is provided, the [TickerProvider] is implicitly obtained using [useSingleTickerProvider]. +/// If a [vsync] is specified, changing the instance of [vsync] will result in a call to [AnimationController.resync]. +/// It is not possible to switch between implicit and explicit [vsync]. +/// +/// Changing the [duration] parameter automatically updates [AnimationController.duration]. +/// +/// [initialValue], [lowerBound], [upperBound] and [debugLabel] are ignored after the first call. +/// +/// See also: +/// * [AnimationController], the created object. +/// * [useAnimation], to listen to the created [AnimationController]. +AnimationController useAnimationController({ + Duration duration, + String debugLabel, + double initialValue = 0, + double lowerBound = 0, + double upperBound = 1, + TickerProvider vsync, + AnimationBehavior animationBehavior = AnimationBehavior.normal, + List keys, +}) { + return Hook.use(_AnimationControllerHook( + duration: duration, + debugLabel: debugLabel, + initialValue: initialValue, + lowerBound: lowerBound, + upperBound: upperBound, + vsync: vsync, + animationBehavior: animationBehavior, + keys: keys, + )); +} + +class _AnimationControllerHook extends Hook { + final Duration duration; + final String debugLabel; + final double initialValue; + final double lowerBound; + final double upperBound; + final TickerProvider vsync; + final AnimationBehavior animationBehavior; + + const _AnimationControllerHook({ + this.duration, + this.debugLabel, + this.initialValue, + this.lowerBound, + this.upperBound, + this.vsync, + this.animationBehavior, + List keys, + }) : super(keys: keys); + + @override + _AnimationControllerHookState createState() => + _AnimationControllerHookState(); +} + +class _AnimationControllerHookState + extends HookState { + AnimationController _animationController; + + @override + void didUpdateHook(_AnimationControllerHook oldHook) { + super.didUpdateHook(oldHook); + if (hook.vsync != oldHook.vsync) { + assert(hook.vsync != null && oldHook.vsync != null, ''' +Switching between controller and uncontrolled vsync is not allowed. +'''); + _animationController.resync(hook.vsync); + } + + if (hook.duration != oldHook.duration) { + _animationController.duration = hook.duration; + } + } + + @override + AnimationController build(BuildContext context) { + final vsync = hook.vsync ?? useSingleTickerProvider(keys: hook.keys); + + _animationController ??= AnimationController( + vsync: vsync, + duration: hook.duration, + debugLabel: hook.debugLabel, + lowerBound: hook.lowerBound, + upperBound: hook.upperBound, + animationBehavior: hook.animationBehavior, + value: hook.initialValue, + ); + + return _animationController; + } + + @override + void dispose() { + super.dispose(); + _animationController.dispose(); + } +} + +/// Creates a single usage [TickerProvider]. +/// +/// See also: +/// * [SingleTickerProviderStateMixin] +TickerProvider useSingleTickerProvider({List keys}) { + return Hook.use( + keys != null + ? _SingleTickerProviderHook(keys) + : const _SingleTickerProviderHook(), + ); +} + +class _SingleTickerProviderHook extends Hook { + const _SingleTickerProviderHook([List keys]) : super(keys: keys); + + @override + _TickerProviderHookState createState() => _TickerProviderHookState(); +} + +class _TickerProviderHookState + extends HookState + implements TickerProvider { + Ticker _ticker; + + @override + Ticker createTicker(TickerCallback onTick) { + assert(() { + if (_ticker == null) return true; + throw FlutterError( + '${context.widget.runtimeType} attempted to use a useSingleTickerProvider multiple times.\n' + 'A SingleTickerProviderStateMixin can only be used as a TickerProvider once. If a ' + 'TickerProvider is used for multiple AnimationController objects, or if it is passed to other ' + 'objects and those objects might use it more than one time in total, then instead of ' + 'using useSingleTickerProvider, use a regular useTickerProvider.'); + }()); + _ticker = Ticker(onTick, debugLabel: 'created by $context'); + return _ticker; + } + + @override + void dispose() { + assert(() { + if (_ticker == null || !_ticker.isActive) return true; + throw FlutterError( + 'useSingleTickerProvider created a Ticker, but at the time ' + 'dispose() was called on the Hook, that Ticker was still active. Tickers used ' + ' by AnimationControllers should be disposed by calling dispose() on ' + ' the AnimationController itself. Otherwise, the ticker will leak.\n'); + }()); + super.dispose(); + } + + @override + TickerProvider build(BuildContext context) { + if (_ticker != null) _ticker.muted = !TickerMode.of(context); + return this; + } +} diff --git a/lib/src/async.dart b/lib/src/async.dart new file mode 100644 index 00000000..e945815a --- /dev/null +++ b/lib/src/async.dart @@ -0,0 +1,275 @@ +part of 'hooks.dart'; + +/// Subscribes to a [Future] and return its current state in an [AsyncSnapshot]. +/// +/// * [preserveState] defines if the current value should be preserved when changing +/// the [Future] instance. +/// +/// See also: +/// * [Future], the listened object. +/// * [useStream], similar to [useFuture] but for [Stream]. +AsyncSnapshot useFuture(Future future, + {T initialData, bool preserveState = true}) { + return Hook.use(_FutureHook(future, + initialData: initialData, preserveState: preserveState)); +} + +class _FutureHook extends Hook> { + final Future future; + final bool preserveState; + final T initialData; + + const _FutureHook(this.future, {this.initialData, this.preserveState = true}); + + @override + _FutureStateHook createState() => _FutureStateHook(); +} + +class _FutureStateHook extends HookState, _FutureHook> { + /// 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; + AsyncSnapshot _snapshot; + + @override + void initHook() { + super.initHook(); + _snapshot = + AsyncSnapshot.withData(ConnectionState.none, hook.initialData); + _subscribe(); + } + + @override + void didUpdateHook(_FutureHook oldHook) { + super.didUpdateHook(oldHook); + if (oldHook.future != hook.future) { + if (_activeCallbackIdentity != null) { + _unsubscribe(); + if (hook.preserveState) { + _snapshot = _snapshot.inState(ConnectionState.none); + } else { + _snapshot = + AsyncSnapshot.withData(ConnectionState.none, hook.initialData); + } + } + _subscribe(); + } + } + + @override + void dispose() { + _unsubscribe(); + super.dispose(); + } + + void _subscribe() { + if (hook.future != null) { + final callbackIdentity = Object(); + _activeCallbackIdentity = callbackIdentity; + hook.future.then((T data) { + if (_activeCallbackIdentity == callbackIdentity) { + setState(() { + _snapshot = AsyncSnapshot.withData(ConnectionState.done, data); + }); + } + }, onError: (Object error) { + if (_activeCallbackIdentity == callbackIdentity) { + setState(() { + _snapshot = AsyncSnapshot.withError(ConnectionState.done, error); + }); + } + }); + _snapshot = _snapshot.inState(ConnectionState.waiting); + } + } + + void _unsubscribe() { + _activeCallbackIdentity = null; + } + + @override + AsyncSnapshot build(BuildContext context) { + return _snapshot; + } +} + +/// Subscribes to a [Stream] and return its current state in an [AsyncSnapshot]. +/// +/// See also: +/// * [Stream], the object listened. +/// * [useFuture], similar to [useStream] but for [Future]. +AsyncSnapshot useStream(Stream stream, + {T initialData, bool preserveState = true}) { + return Hook.use(_StreamHook( + stream, + initialData: initialData, + preserveState: preserveState, + )); +} + +class _StreamHook extends Hook> { + final Stream stream; + final T initialData; + final bool preserveState; + + _StreamHook(this.stream, {this.initialData, this.preserveState = true}); + + @override + _StreamHookState createState() => _StreamHookState(); +} + +/// a clone of [StreamBuilderBase] implementation +class _StreamHookState extends HookState, _StreamHook> { + StreamSubscription _subscription; + AsyncSnapshot _summary; + + @override + void initHook() { + super.initHook(); + _summary = initial(); + _subscribe(); + } + + @override + void didUpdateHook(_StreamHook oldWidget) { + super.didUpdateHook(oldWidget); + if (oldWidget.stream != hook.stream) { + if (_subscription != null) { + _unsubscribe(); + if (hook.preserveState) { + _summary = afterDisconnected(_summary); + } else { + _summary = initial(); + } + } + _subscribe(); + } + } + + @override + void dispose() { + _unsubscribe(); + super.dispose(); + } + + void _subscribe() { + if (hook.stream != null) { + _subscription = hook.stream.listen((T data) { + setState(() { + _summary = afterData(_summary, data); + }); + }, onError: (Object error) { + setState(() { + _summary = afterError(_summary, error); + }); + }, onDone: () { + setState(() { + _summary = afterDone(_summary); + }); + }); + _summary = afterConnected(_summary); + } + } + + void _unsubscribe() { + if (_subscription != null) { + _subscription.cancel(); + _subscription = null; + } + } + + @override + AsyncSnapshot build(BuildContext context) { + return _summary; + } + + AsyncSnapshot initial() => + AsyncSnapshot.withData(ConnectionState.none, hook.initialData); + + AsyncSnapshot afterConnected(AsyncSnapshot current) => + current.inState(ConnectionState.waiting); + + AsyncSnapshot afterData(AsyncSnapshot current, T data) { + return AsyncSnapshot.withData(ConnectionState.active, data); + } + + AsyncSnapshot afterError(AsyncSnapshot current, Object error) { + return AsyncSnapshot.withError(ConnectionState.active, error); + } + + AsyncSnapshot afterDone(AsyncSnapshot current) => + current.inState(ConnectionState.done); + + AsyncSnapshot afterDisconnected(AsyncSnapshot current) => + current.inState(ConnectionState.none); +} + +/// Creates a [StreamController] automatically disposed. +/// +/// See also: +/// * [StreamController], the created object +/// * [useStream], to listen to the created [StreamController] +StreamController useStreamController( + {bool sync = false, + VoidCallback onListen, + VoidCallback onCancel, + List keys}) { + return Hook.use(_StreamControllerHook( + onCancel: onCancel, + onListen: onListen, + sync: sync, + keys: keys, + )); +} + +class _StreamControllerHook extends Hook> { + final bool sync; + final VoidCallback onListen; + final VoidCallback onCancel; + + const _StreamControllerHook( + {this.sync = false, this.onListen, this.onCancel, List keys}) + : super(keys: keys); + + @override + _StreamControllerHookState createState() => + _StreamControllerHookState(); +} + +class _StreamControllerHookState + extends HookState, _StreamControllerHook> { + StreamController _controller; + + @override + void initHook() { + super.initHook(); + _controller = StreamController.broadcast( + sync: hook.sync, + onCancel: hook.onCancel, + onListen: hook.onListen, + ); + } + + @override + void didUpdateHook(_StreamControllerHook oldHook) { + super.didUpdateHook(oldHook); + if (oldHook.onListen != hook.onListen) { + _controller.onListen = hook.onListen; + } + if (oldHook.onCancel != hook.onCancel) { + _controller.onCancel = hook.onCancel; + } + } + + @override + StreamController build(BuildContext context) { + return _controller; + } + + @override + void dispose() { + _controller.close(); + super.dispose(); + } +} diff --git a/lib/src/framework.dart b/lib/src/framework.dart index 324d9983..000cfc74 100644 --- a/lib/src/framework.dart +++ b/lib/src/framework.dart @@ -1,11 +1,7 @@ -import 'dart:async'; - import 'package:flutter/foundation.dart'; import 'package:flutter/scheduler.dart'; import 'package:flutter/widgets.dart'; -part 'hooks.dart'; - /// [Hook] is similar to a [StatelessWidget], but is not associated /// to an [Element]. /// @@ -493,3 +489,31 @@ class _HookWidgetState extends State { return widget.build(context); } } + +/// Obtain the [BuildContext] of the building [HookWidget]. +BuildContext useContext() { + assert(HookElement._currentContext != null, + '`useContext` can only be called from the build method of HookWidget'); + return HookElement._currentContext; +} + +/// A [HookWidget] that defer its [HookWidget.build] to a callback +class HookBuilder extends HookWidget { + /// The callback used by [HookBuilder] to create a widget. + /// + /// If a [Hook] asks for a rebuild, [builder] will be called again. + /// [builder] must not return `null`. + final Widget Function(BuildContext context) builder; + + /// Creates a widget that delegates its build to a callback. + /// + /// The [builder] argument must not be null. + const HookBuilder({ + @required this.builder, + Key key, + }) : assert(builder != null), + super(key: key); + + @override + Widget build(BuildContext context) => builder(context); +} diff --git a/lib/src/hooks.dart b/lib/src/hooks.dart index 75ffe134..c6009c42 100644 --- a/lib/src/hooks.dart +++ b/lib/src/hooks.dart @@ -1,852 +1,11 @@ -part of 'framework.dart'; - -/// A [HookWidget] that defer its [HookWidget.build] to a callback -class HookBuilder extends HookWidget { - /// The callback used by [HookBuilder] to create a widget. - /// - /// If a [Hook] asks for a rebuild, [builder] will be called again. - /// [builder] must not return `null`. - final Widget Function(BuildContext context) builder; - - /// Creates a widget that delegates its build to a callback. - /// - /// The [builder] argument must not be null. - const HookBuilder({ - @required this.builder, - Key key, - }) : assert(builder != null), - super(key: key); - - @override - Widget build(BuildContext context) => builder(context); -} - -/// A state holder that allows mutations by dispatching actions. -abstract class Store { - /// The current state. - /// - /// This value may change after a call to [dispatch]. - State get state; - - /// Dispatches an action. - /// - /// Actions are dispatched synchronously. - /// It is impossible to try to dispatch actions during [HookWidget.build]. - void dispatch(Action action); -} - -/// Composes an [Action] and a [State] to create a new [State]. -/// -/// [Reducer] must never return `null`, even if [state] or [action] are `null`. -typedef Reducer = State Function(State state, Action action); - -/// An alternative to [useState] for more complex states. -/// -/// [useReducer] manages an read only state that can be updated -/// by dispatching actions which are interpreted by a [Reducer]. -/// -/// [reducer] is immediatly called on first build with [initialAction] -/// and [initialState] as parameter. -/// -/// It is possible to change the [reducer] by calling [useReducer] -/// with a new [Reducer]. -/// -/// See also: -/// * [Reducer] -/// * [Store] -Store useReducer( - Reducer reducer, { - State initialState, - Action initialAction, -}) { - return Hook.use(_ReducerdHook(reducer, - initialAction: initialAction, initialState: initialState)); -} - -class _ReducerdHook extends Hook> { - final Reducer reducer; - final State initialState; - final Action initialAction; - - const _ReducerdHook(this.reducer, {this.initialState, this.initialAction}) - : assert(reducer != null); - - @override - _ReducerdHookState createState() => - _ReducerdHookState(); -} - -class _ReducerdHookState - extends HookState, _ReducerdHook> - implements Store { - @override - State state; - - @override - void initHook() { - super.initHook(); - state = hook.reducer(hook.initialState, hook.initialAction); - assert(state != null); - } - - @override - void dispatch(Action action) { - final res = hook.reducer(state, action); - assert(res != null); - if (state != res) { - setState(() { - state = res; - }); - } - } - - @override - Store build(BuildContext context) { - return this; - } -} - -/// Create and cache the instance of an object. -/// -/// [useMemoized] will immediatly call [valueBuilder] on first call and store its result. -/// Later calls to [useMemoized] will reuse the created instance. -/// -/// * [keys] can be use to specify a list of objects for [useMemoized] to watch. -/// So that whenever [Object.operator==] fails on any parameter or if the length of [keys] changes, -/// [valueBuilder] is called again. -T useMemoized(T Function() valueBuilder, [List keys = const []]) { - return Hook.use(_MemoizedHook( - valueBuilder, - keys: keys, - )); -} - -/// Obtain the [BuildContext] of the currently builder [HookWidget]. -BuildContext useContext() { - assert(HookElement._currentContext != null, - '`useContext` can only be called from the build method of HookWidget'); - return HookElement._currentContext; -} - -class _MemoizedHook extends Hook { - final T Function() valueBuilder; - - const _MemoizedHook(this.valueBuilder, {List keys = const []}) - : assert(valueBuilder != null), - assert(keys != null), - super(keys: keys); - - @override - _MemoizedHookState createState() => _MemoizedHookState(); -} - -class _MemoizedHookState extends HookState> { - T value; - - @override - void initHook() { - super.initHook(); - value = hook.valueBuilder(); - } - - @override - T build(BuildContext context) { - return value; - } -} - -/// Watches a value. -/// -/// Whenever [useValueChanged] is called with a diffent [value], calls [valueChange]. -/// The value returned by [useValueChanged] is the latest returned value of [valueChange] or `null`. -R useValueChanged(T value, R valueChange(T oldValue, R oldResult)) { - return Hook.use(_ValueChangedHook(value, valueChange)); -} - -class _ValueChangedHook extends Hook { - final R Function(T oldValue, R oldResult) valueChanged; - final T value; - - const _ValueChangedHook(this.value, this.valueChanged) - : assert(valueChanged != null); - - @override - _ValueChangedHookState createState() => _ValueChangedHookState(); -} - -class _ValueChangedHookState - extends HookState> { - R _result; - - @override - void didUpdateHook(_ValueChangedHook oldHook) { - super.didUpdateHook(oldHook); - if (hook.value != oldHook.value) { - _result = hook.valueChanged(oldHook.value, _result); - } - } - - @override - R build(BuildContext context) { - return _result; - } -} - -/// Create value and subscribes to it. -/// -/// Whenever [ValueNotifier.value] updates, it will mark the caller [HookWidget] -/// as needing build. -/// On first call, inits [ValueNotifier] to [initialData]. [initialData] is ignored -/// on subsequent calls. -/// -/// See also: -/// -/// * [ValueNotifier] -/// * [useStreamController], an alternative to [ValueNotifier] for state. -ValueNotifier useState([T initialData]) { - return Hook.use(_StateHook(initialData: initialData)); -} - -class _StateHook extends Hook> { - final T initialData; - - const _StateHook({this.initialData}); - - @override - _StateHookState createState() => _StateHookState(); -} - -class _StateHookState extends HookState, _StateHook> { - ValueNotifier _state; - - @override - void initHook() { - super.initHook(); - _state = ValueNotifier(hook.initialData)..addListener(_listener); - } - - @override - void dispose() { - _state.dispose(); - super.dispose(); - } - - @override - ValueNotifier build(BuildContext context) { - return _state; - } - - void _listener() { - setState(() {}); - } -} - -/// Creates a single usage [TickerProvider]. -/// -/// See also: -/// * [SingleTickerProviderStateMixin] -TickerProvider useSingleTickerProvider({List keys}) { - return Hook.use( - keys != null - ? _SingleTickerProviderHook(keys) - : const _SingleTickerProviderHook(), - ); -} - -class _SingleTickerProviderHook extends Hook { - const _SingleTickerProviderHook([List keys]) : super(keys: keys); - - @override - _TickerProviderHookState createState() => _TickerProviderHookState(); -} - -class _TickerProviderHookState - extends HookState - implements TickerProvider { - Ticker _ticker; - - @override - Ticker createTicker(TickerCallback onTick) { - assert(() { - if (_ticker == null) return true; - throw FlutterError( - '${context.widget.runtimeType} attempted to use a useSingleTickerProvider multiple times.\n' - 'A SingleTickerProviderStateMixin can only be used as a TickerProvider once. If a ' - 'TickerProvider is used for multiple AnimationController objects, or if it is passed to other ' - 'objects and those objects might use it more than one time in total, then instead of ' - 'using useSingleTickerProvider, use a regular useTickerProvider.'); - }()); - _ticker = Ticker(onTick, debugLabel: 'created by $context'); - return _ticker; - } - - @override - void dispose() { - assert(() { - if (_ticker == null || !_ticker.isActive) return true; - throw FlutterError( - 'useSingleTickerProvider created a Ticker, but at the time ' - 'dispose() was called on the Hook, that Ticker was still active. Tickers used ' - ' by AnimationControllers should be disposed by calling dispose() on ' - ' the AnimationController itself. Otherwise, the ticker will leak.\n'); - }()); - super.dispose(); - } - - @override - TickerProvider build(BuildContext context) { - if (_ticker != null) _ticker.muted = !TickerMode.of(context); - return this; - } -} - -/// Creates an [AnimationController] automatically disposed. -/// -/// If no [vsync] is provided, the [TickerProvider] is implicitly obtained using [useSingleTickerProvider]. -/// If a [vsync] is specified, changing the instance of [vsync] will result in a call to [AnimationController.resync]. -/// It is not possible to switch between implicit and explicit [vsync]. -/// -/// Changing the [duration] parameter automatically updates [AnimationController.duration]. -/// -/// [initialValue], [lowerBound], [upperBound] and [debugLabel] are ignored after the first call. -/// -/// See also: -/// * [AnimationController] -/// * [useAnimation] -AnimationController useAnimationController({ - Duration duration, - String debugLabel, - double initialValue = 0, - double lowerBound = 0, - double upperBound = 1, - TickerProvider vsync, - AnimationBehavior animationBehavior = AnimationBehavior.normal, - List keys, -}) { - return Hook.use(_AnimationControllerHook( - duration: duration, - debugLabel: debugLabel, - initialValue: initialValue, - lowerBound: lowerBound, - upperBound: upperBound, - vsync: vsync, - animationBehavior: animationBehavior, - keys: keys, - )); -} - -class _AnimationControllerHook extends Hook { - final Duration duration; - final String debugLabel; - final double initialValue; - final double lowerBound; - final double upperBound; - final TickerProvider vsync; - final AnimationBehavior animationBehavior; - - const _AnimationControllerHook({ - this.duration, - this.debugLabel, - this.initialValue, - this.lowerBound, - this.upperBound, - this.vsync, - this.animationBehavior, - List keys, - }) : super(keys: keys); - - @override - _AnimationControllerHookState createState() => - _AnimationControllerHookState(); -} - -class _AnimationControllerHookState - extends HookState { - AnimationController _animationController; - - @override - void didUpdateHook(_AnimationControllerHook oldHook) { - super.didUpdateHook(oldHook); - if (hook.vsync != oldHook.vsync) { - assert(hook.vsync != null && oldHook.vsync != null, ''' -Switching between controller and uncontrolled vsync is not allowed. -'''); - _animationController.resync(hook.vsync); - } - - if (hook.duration != oldHook.duration) { - _animationController.duration = hook.duration; - } - } - - @override - AnimationController build(BuildContext context) { - final vsync = hook.vsync ?? useSingleTickerProvider(keys: hook.keys); - - _animationController ??= AnimationController( - vsync: vsync, - duration: hook.duration, - debugLabel: hook.debugLabel, - lowerBound: hook.lowerBound, - upperBound: hook.upperBound, - animationBehavior: hook.animationBehavior, - value: hook.initialValue, - ); - - return _animationController; - } - - @override - void dispose() { - super.dispose(); - _animationController.dispose(); - } -} - -/// Subscribes to a [ValueListenable] and return its value. -/// -/// See also: -/// * [ValueListenable] -/// * [useListenable], [useAnimation], [useStream] -T useValueListenable(ValueListenable valueListenable) { - useListenable(valueListenable); - return valueListenable.value; -} - -/// Subscribes to a [Listenable] and mark the widget as needing build -/// whenever the listener is called. -/// -/// See also: -/// * [Listenable] -/// * [useValueListenable], [useAnimation], [useStream] -void useListenable(Listenable listenable) { - Hook.use(_ListenableHook(listenable)); -} - -/// Subscribes to an [Animation] and return its value. -/// -/// See also: -/// * [Animation] -/// * [useValueListenable], [useListenable], [useStream] -T useAnimation(Animation animation) { - useListenable(animation); - return animation.value; -} - -class _ListenableHook extends Hook { - final Listenable listenable; - - const _ListenableHook(this.listenable) : assert(listenable != null); - - @override - _ListenableStateHook createState() => _ListenableStateHook(); -} - -class _ListenableStateHook extends HookState { - @override - void initHook() { - super.initHook(); - hook.listenable.addListener(_listener); - } - - @override - void didUpdateHook(_ListenableHook oldHook) { - super.didUpdateHook(oldHook); - if (hook.listenable != oldHook.listenable) { - oldHook.listenable.removeListener(_listener); - hook.listenable.addListener(_listener); - } - } - - @override - void build(BuildContext context) {} - - void _listener() { - setState(() {}); - } - - @override - void dispose() { - super.dispose(); - hook.listenable.removeListener(_listener); - } -} - -/// Subscribes to a [Future] and return its current state in an [AsyncSnapshot]. -/// -/// * [preserveState] defines if the current value should be preserved when changing -/// the [Future] instance. -/// -/// See also: -/// * [Future] -/// * [useValueListenable], [useListenable], [useAnimation] -AsyncSnapshot useFuture(Future future, - {T initialData, bool preserveState = true}) { - return Hook.use(_FutureHook(future, - initialData: initialData, preserveState: preserveState)); -} - -class _FutureHook extends Hook> { - final Future future; - final bool preserveState; - final T initialData; - - const _FutureHook(this.future, {this.initialData, this.preserveState = true}); - - @override - _FutureStateHook createState() => _FutureStateHook(); -} - -class _FutureStateHook extends HookState, _FutureHook> { - /// 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; - AsyncSnapshot _snapshot; - - @override - void initHook() { - super.initHook(); - _snapshot = - AsyncSnapshot.withData(ConnectionState.none, hook.initialData); - _subscribe(); - } - - @override - void didUpdateHook(_FutureHook oldHook) { - super.didUpdateHook(oldHook); - if (oldHook.future != hook.future) { - if (_activeCallbackIdentity != null) { - _unsubscribe(); - if (hook.preserveState) { - _snapshot = _snapshot.inState(ConnectionState.none); - } else { - _snapshot = - AsyncSnapshot.withData(ConnectionState.none, hook.initialData); - } - } - _subscribe(); - } - } - - @override - void dispose() { - _unsubscribe(); - super.dispose(); - } - - void _subscribe() { - if (hook.future != null) { - final callbackIdentity = Object(); - _activeCallbackIdentity = callbackIdentity; - hook.future.then((T data) { - if (_activeCallbackIdentity == callbackIdentity) { - setState(() { - _snapshot = AsyncSnapshot.withData(ConnectionState.done, data); - }); - } - }, onError: (Object error) { - if (_activeCallbackIdentity == callbackIdentity) { - setState(() { - _snapshot = AsyncSnapshot.withError(ConnectionState.done, error); - }); - } - }); - _snapshot = _snapshot.inState(ConnectionState.waiting); - } - } - - void _unsubscribe() { - _activeCallbackIdentity = null; - } - - @override - AsyncSnapshot build(BuildContext context) { - return _snapshot; - } -} - -/// Subscribes to a [Stream] and return its current state in an [AsyncSnapshot]. -/// -/// See also: -/// * [Stream] -/// * [useValueListenable], [useListenable], [useAnimation] -AsyncSnapshot useStream(Stream stream, - {T initialData, bool preserveState = true}) { - return Hook.use(_StreamHook( - stream, - initialData: initialData, - preserveState: preserveState, - )); -} - -class _StreamHook extends Hook> { - final Stream stream; - final T initialData; - final bool preserveState; - - _StreamHook(this.stream, {this.initialData, this.preserveState = true}); - - @override - _StreamHookState createState() => _StreamHookState(); -} - -/// a clone of [StreamBuilderBase] implementation -class _StreamHookState extends HookState, _StreamHook> { - StreamSubscription _subscription; - AsyncSnapshot _summary; - - @override - void initHook() { - super.initHook(); - _summary = initial(); - _subscribe(); - } - - @override - void didUpdateHook(_StreamHook oldWidget) { - super.didUpdateHook(oldWidget); - if (oldWidget.stream != hook.stream) { - if (_subscription != null) { - _unsubscribe(); - if (hook.preserveState) { - _summary = afterDisconnected(_summary); - } else { - _summary = initial(); - } - } - _subscribe(); - } - } - - @override - void dispose() { - _unsubscribe(); - super.dispose(); - } - - void _subscribe() { - if (hook.stream != null) { - _subscription = hook.stream.listen((T data) { - setState(() { - _summary = afterData(_summary, data); - }); - }, onError: (Object error) { - setState(() { - _summary = afterError(_summary, error); - }); - }, onDone: () { - setState(() { - _summary = afterDone(_summary); - }); - }); - _summary = afterConnected(_summary); - } - } - - void _unsubscribe() { - if (_subscription != null) { - _subscription.cancel(); - _subscription = null; - } - } - - @override - AsyncSnapshot build(BuildContext context) { - return _summary; - } - - AsyncSnapshot initial() => - AsyncSnapshot.withData(ConnectionState.none, hook.initialData); - - AsyncSnapshot afterConnected(AsyncSnapshot current) => - current.inState(ConnectionState.waiting); - - AsyncSnapshot afterData(AsyncSnapshot current, T data) { - return AsyncSnapshot.withData(ConnectionState.active, data); - } - - AsyncSnapshot afterError(AsyncSnapshot current, Object error) { - return AsyncSnapshot.withError(ConnectionState.active, error); - } - - AsyncSnapshot afterDone(AsyncSnapshot current) => - current.inState(ConnectionState.done); - - AsyncSnapshot afterDisconnected(AsyncSnapshot current) => - current.inState(ConnectionState.none); -} - -typedef Dispose = void Function(); - -/// A hook for side-effects -/// -/// [useEffect] is called synchronously on every [HookWidget.build], unless -/// [keys] is specified. In which case [useEffect] is called again only if -/// any value inside [keys] as changed. -void useEffect(Dispose Function() effect, [List keys]) { - Hook.use(_EffectHook(effect, keys)); -} - -class _EffectHook extends Hook { - final Dispose Function() effect; - - const _EffectHook(this.effect, [List keys]) - : assert(effect != null), - super(keys: keys); - - @override - _EffectHookState createState() => _EffectHookState(); -} - -class _EffectHookState extends HookState { - Dispose disposer; - - @override - void initHook() { - super.initHook(); - scheduleEffect(); - } - - @override - void didUpdateHook(_EffectHook oldHook) { - super.didUpdateHook(oldHook); - - if (hook.keys == null) { - if (disposer != null) { - disposer(); - } - scheduleEffect(); - } - } - - @override - void build(BuildContext context) {} - - @override - void dispose() { - if (disposer != null) { - disposer(); - } - super.dispose(); - } - - void scheduleEffect() { - disposer = hook.effect(); - } -} - -/// Creates a [StreamController] automatically disposed. -/// -/// See also: -/// * [StreamController] -/// * [useStream] -StreamController useStreamController( - {bool sync = false, - VoidCallback onListen, - VoidCallback onCancel, - List keys}) { - return Hook.use(_StreamControllerHook( - onCancel: onCancel, - onListen: onListen, - sync: sync, - keys: keys, - )); -} - -class _StreamControllerHook extends Hook> { - final bool sync; - final VoidCallback onListen; - final VoidCallback onCancel; - - const _StreamControllerHook( - {this.sync = false, this.onListen, this.onCancel, List keys}) - : super(keys: keys); - - @override - _StreamControllerHookState createState() => - _StreamControllerHookState(); -} - -class _StreamControllerHookState - extends HookState, _StreamControllerHook> { - StreamController _controller; - - @override - void initHook() { - super.initHook(); - _controller = StreamController.broadcast( - sync: hook.sync, - onCancel: hook.onCancel, - onListen: hook.onListen, - ); - } - - @override - void didUpdateHook(_StreamControllerHook oldHook) { - super.didUpdateHook(oldHook); - if (oldHook.onListen != hook.onListen) { - _controller.onListen = hook.onListen; - } - if (oldHook.onCancel != hook.onCancel) { - _controller.onCancel = hook.onCancel; - } - } - - @override - StreamController build(BuildContext context) { - return _controller; - } - - @override - void dispose() { - _controller.close(); - super.dispose(); - } -} - -/// Creates a [ValueNotifier] automatically disposed. -/// -/// As opposed to `useState`, this hook do not subscribes to [ValueNotifier]. -/// This allows a more granular rebuild. -/// -/// See also: -/// * [ValueNotifier] -/// * [useValueListenable] -ValueNotifier useValueNotifier([T intialData, List keys]) { - return Hook.use(_ValueNotifierHook( - initialData: intialData, - keys: keys, - )); -} - -class _ValueNotifierHook extends Hook> { - final T initialData; - - const _ValueNotifierHook({List keys, this.initialData}) : super(keys: keys); - - @override - _UseValueNotiferHookState createState() => _UseValueNotiferHookState(); -} - -class _UseValueNotiferHookState - extends HookState, _ValueNotifierHook> { - ValueNotifier notifier; - - @override - void initHook() { - super.initHook(); - notifier = ValueNotifier(hook.initialData); - } - - @override - ValueNotifier build(BuildContext context) { - return notifier; - } - - @override - void dispose() { - notifier.dispose(); - super.dispose(); - } -} +import 'dart:async'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/scheduler.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_hooks/src/framework.dart'; +part 'async.dart'; +part 'animation.dart'; +part 'misc.dart'; +part 'primitives.dart'; +part 'listenable.dart'; diff --git a/lib/src/listenable.dart b/lib/src/listenable.dart new file mode 100644 index 00000000..8a2ae3bd --- /dev/null +++ b/lib/src/listenable.dart @@ -0,0 +1,107 @@ +part of 'hooks.dart'; + +/// Subscribes to a [ValueListenable] and return its value. +/// +/// See also: +/// * [ValueListenable], the created object +/// * [useListenable] +T useValueListenable(ValueListenable valueListenable) { + return useListenable(valueListenable).value; +} + +/// Subscribes to a [Listenable] and mark the widget as needing build +/// whenever the listener is called. +/// +/// See also: +/// * [Listenable] +/// * [useValueListenable], [useAnimation] +T useListenable(T listenable) { + Hook.use(_ListenableHook(listenable)); + return listenable; +} + +class _ListenableHook extends Hook { + final Listenable listenable; + + const _ListenableHook(this.listenable) : assert(listenable != null); + + @override + _ListenableStateHook createState() => _ListenableStateHook(); +} + +class _ListenableStateHook extends HookState { + @override + void initHook() { + super.initHook(); + hook.listenable.addListener(_listener); + } + + @override + void didUpdateHook(_ListenableHook oldHook) { + super.didUpdateHook(oldHook); + if (hook.listenable != oldHook.listenable) { + oldHook.listenable.removeListener(_listener); + hook.listenable.addListener(_listener); + } + } + + @override + void build(BuildContext context) {} + + void _listener() { + setState(() {}); + } + + @override + void dispose() { + super.dispose(); + hook.listenable.removeListener(_listener); + } +} + +/// Creates a [ValueNotifier] automatically disposed. +/// +/// As opposed to `useState`, this hook do not subscribes to [ValueNotifier]. +/// This allows a more granular rebuild. +/// +/// See also: +/// * [ValueNotifier] +/// * [useValueListenable] +ValueNotifier useValueNotifier([T intialData, List keys]) { + return Hook.use(_ValueNotifierHook( + initialData: intialData, + keys: keys, + )); +} + +class _ValueNotifierHook extends Hook> { + final T initialData; + + const _ValueNotifierHook({List keys, this.initialData}) + : super(keys: keys); + + @override + _UseValueNotiferHookState createState() => _UseValueNotiferHookState(); +} + +class _UseValueNotiferHookState + extends HookState, _ValueNotifierHook> { + ValueNotifier notifier; + + @override + void initHook() { + super.initHook(); + notifier = ValueNotifier(hook.initialData); + } + + @override + ValueNotifier build(BuildContext context) { + return notifier; + } + + @override + void dispose() { + notifier.dispose(); + super.dispose(); + } +} diff --git a/lib/src/misc.dart b/lib/src/misc.dart new file mode 100644 index 00000000..3cc636c0 --- /dev/null +++ b/lib/src/misc.dart @@ -0,0 +1,86 @@ +part of 'hooks.dart'; + +/// A state holder that allows mutations by dispatching actions. +abstract class Store { + /// The current state. + /// + /// This value may change after a call to [dispatch]. + State get state; + + /// Dispatches an action. + /// + /// Actions are dispatched synchronously. + /// It is impossible to try to dispatch actions during [HookWidget.build]. + void dispatch(Action action); +} + +/// Composes an [Action] and a [State] to create a new [State]. +/// +/// [Reducer] must never return `null`, even if [state] or [action] are `null`. +typedef Reducer = State Function(State state, Action action); + +/// An alternative to [useState] for more complex states. +/// +/// [useReducer] manages an read only state that can be updated +/// by dispatching actions which are interpreted by a [Reducer]. +/// +/// [reducer] is immediatly called on first build with [initialAction] +/// and [initialState] as parameter. +/// +/// It is possible to change the [reducer] by calling [useReducer] +/// with a new [Reducer]. +/// +/// See also: +/// * [Reducer] +/// * [Store] +Store useReducer( + Reducer reducer, { + State initialState, + Action initialAction, +}) { + return Hook.use(_ReducerdHook(reducer, + initialAction: initialAction, initialState: initialState)); +} + +class _ReducerdHook extends Hook> { + final Reducer reducer; + final State initialState; + final Action initialAction; + + const _ReducerdHook(this.reducer, {this.initialState, this.initialAction}) + : assert(reducer != null); + + @override + _ReducerdHookState createState() => + _ReducerdHookState(); +} + +class _ReducerdHookState + extends HookState, _ReducerdHook> + implements Store { + @override + State state; + + @override + void initHook() { + super.initHook(); + state = hook.reducer(hook.initialState, hook.initialAction); + assert(state != null); + } + + @override + void dispatch(Action action) { + final res = hook.reducer(state, action); + assert(res != null); + if (state != res) { + setState(() { + state = res; + }); + } + } + + @override + Store build(BuildContext context) { + return this; + } +} diff --git a/lib/src/primitives.dart b/lib/src/primitives.dart new file mode 100644 index 00000000..a2a53328 --- /dev/null +++ b/lib/src/primitives.dart @@ -0,0 +1,242 @@ +part of 'hooks.dart'; + +/// Cache the instance of a complex object. +/// +/// [useMemoized] will immediatly call [valueBuilder] on first call and store its result. +/// Later, when [HookWidget] rebuilds, the call to [useMemoized] will return the previously created instance without calling [valueBuilder]. +/// +/// A later call of [useMemoized] with different [keys] will call [useMemoized] again to create a new instance. +T useMemoized(T Function() valueBuilder, + [List keys = const []]) { + return Hook.use(_MemoizedHook( + valueBuilder, + keys: keys, + )); +} + +class _MemoizedHook extends Hook { + final T Function() valueBuilder; + + const _MemoizedHook(this.valueBuilder, + {List keys = const []}) + : assert(valueBuilder != null), + assert(keys != null), + super(keys: keys); + + @override + _MemoizedHookState createState() => _MemoizedHookState(); +} + +class _MemoizedHookState extends HookState> { + T value; + + @override + void initHook() { + super.initHook(); + value = hook.valueBuilder(); + } + + @override + T build(BuildContext context) { + return value; + } +} + +/// Watches a value and calls a callback whenever the value changed. +/// +/// [useValueChanged] takes a [valueChange] callback and calls it whenever [value] changed. +/// [valueChange] will _not_ be called on the first [useValueChanged] call. +/// +/// [useValueChanged] can also be used to interpolate +/// Whenever [useValueChanged] is called with a diffent [value], calls [valueChange]. +/// The value returned by [useValueChanged] is the latest returned value of [valueChange] or `null`. +/// +/// The following example calls [AnimationController.forward] whenever `color` changes +/// +/// ```dart +/// AnimationController controller; +/// Color color; +/// +/// useValueChanged(color, (_, __)) { +/// controller.forward(); +/// }); +/// ``` +R useValueChanged(T value, R valueChange(T oldValue, R oldResult)) { + return Hook.use(_ValueChangedHook(value, valueChange)); +} + +class _ValueChangedHook extends Hook { + final R Function(T oldValue, R oldResult) valueChanged; + final T value; + + const _ValueChangedHook(this.value, this.valueChanged) + : assert(valueChanged != null); + + @override + _ValueChangedHookState createState() => _ValueChangedHookState(); +} + +class _ValueChangedHookState + extends HookState> { + R _result; + + @override + void didUpdateHook(_ValueChangedHook oldHook) { + super.didUpdateHook(oldHook); + if (hook.value != oldHook.value) { + _result = hook.valueChanged(oldHook.value, _result); + } + } + + @override + R build(BuildContext context) { + return _result; + } +} + +/// Useful for side-effects and optionally canceling them. +/// +/// [useEffect] is called synchronously on every [HookWidget.build], unless +typedef Dispose = void Function(); + +/// [keys] is specified. In which case [useEffect] is called again only if +/// any value inside [keys] as changed. +/// +/// It takes an [effect] callback and calls it synchronously. +/// That [effect] may optionally return a function, which will be called when the [effect] is called again or if the widget is disposed. +/// +/// By default [effect] is called on every [HookWidget.build] call, unless [keys] is specified. +/// In which case, [effect] is called once on the first [useEffect] call and whenever something within [keys] change/ +/// +/// The following example call [useEffect] to subscribes to a [Stream] and cancel the subscription when the widget is disposed. +/// ALso ifthe [Stream] change, it will cancel the listening on the previous [Stream] and listen to the new one. +/// +/// ```dart +/// Stream stream; +/// useEffect(() { +/// final subscription = stream.listen(print); +/// // This will cancel the subscription when the widget is disposed +/// // or if the callback is called again. +/// return subscription.cancel; +/// }, +/// // when the stream change, useEffect will call the callback again. +/// [stream], +/// ); +/// ``` +void useEffect(Dispose Function() effect, [List keys]) { + Hook.use(_EffectHook(effect, keys)); +} + +class _EffectHook extends Hook { + final Dispose Function() effect; + + const _EffectHook(this.effect, [List keys]) + : assert(effect != null), + super(keys: keys); + + @override + _EffectHookState createState() => _EffectHookState(); +} + +class _EffectHookState extends HookState { + Dispose disposer; + + @override + void initHook() { + super.initHook(); + scheduleEffect(); + } + + @override + void didUpdateHook(_EffectHook oldHook) { + super.didUpdateHook(oldHook); + + if (hook.keys == null) { + if (disposer != null) { + disposer(); + } + scheduleEffect(); + } + } + + @override + void build(BuildContext context) {} + + @override + void dispose() { + if (disposer != null) { + disposer(); + } + super.dispose(); + } + + void scheduleEffect() { + disposer = hook.effect(); + } +} + +/// Create variable and subscribes to it. +/// +/// Whenever [ValueNotifier.value] updates, it will mark the caller [HookWidget] +/// as needing build. +/// On first call, inits [ValueNotifier] to [initialData]. [initialData] is ignored +/// on subsequent calls. +/// +/// The following example showcase a basic counter application. +/// +/// ```dart +/// class Counter extends HookWidget { +/// @override +/// Widget build(BuildContext context) { +/// final counter = useState(0); +/// +/// return GestureDetector( +/// // automatically triggers a rebuild of Counter widget +/// onTap: () => counter.value++, +/// child: Text(counter.value.toString()), +/// ); +/// } +/// } +/// ``` +/// +/// See also: +/// +/// * [ValueNotifier] +/// * [useStreamController], an alternative to [ValueNotifier] for state. +ValueNotifier useState([T initialData]) { + return Hook.use(_StateHook(initialData: initialData)); +} + +class _StateHook extends Hook> { + final T initialData; + + const _StateHook({this.initialData}); + + @override + _StateHookState createState() => _StateHookState(); +} + +class _StateHookState extends HookState, _StateHook> { + ValueNotifier _state; + + @override + void initHook() { + super.initHook(); + _state = ValueNotifier(hook.initialData)..addListener(_listener); + } + + @override + void dispose() { + _state.dispose(); + super.dispose(); + } + + @override + ValueNotifier build(BuildContext context) { + return _state; + } + + void _listener() { + setState(() {}); + } +} diff --git a/test/mock.dart b/test/mock.dart index be7ec820..6df09d0e 100644 --- a/test/mock.dart +++ b/test/mock.dart @@ -42,7 +42,7 @@ class HookTest extends Hook { this.reassemble, this.createStateFn, this.didBuild, - List keys, + List keys, }) : super(keys: keys); @override From 9cbd4b7fbd7cb3d0a27cfb6ed83fad12cfd34c37 Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Fri, 5 Apr 2019 12:29:34 +0200 Subject: [PATCH 063/384] fix markdown --- README.md | 28 +++++++++++++++++----------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index bfcc3fc7..de620201 100644 --- a/README.md +++ b/README.md @@ -304,30 +304,36 @@ They will take care of creating/updating/disposing an object. | name | description | | ------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------- | -| [useStream](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useStream.html) | Subscribes to a [Stream] and return its current state in an [AsyncSnapshot]. | -| [useStreamController](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useStreamController.html) | Creates a [StreamController] automatically disposed. | -| [useFuture](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useFuture.html) | Subscribes to a [Future] and return its current state in an [AsyncSnapshot]. | +| [useStream](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useStream.html) | Subscribes to a `Stream` and return its current state in an `AsyncSnapshot`. | +| [useStreamController](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useStreamController.html) | Creates a `StreamController` automatically disposed. | +| [useFuture](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useFuture.html) | Subscribes to a `Future` and return its current state in an `AsyncSnapshot`. | #### Animation related: | name | description | | --------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------- | -| [useSingleTickerProvider](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useSingleTickerProvider.html) | Creates a single usage [TickerProvider]. | -| [useAnimationController](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useAnimationController.html) | Creates an [AnimationController] automatically disposed. | -| [useAnimation](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useAnimation.html) | Subscribes to an [Animation] and return its value. | +| [useSingleTickerProvider](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useSingleTickerProvider.html) | Creates a single usage `TickerProvider`. | +| [useAnimationController](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useAnimationController.html) | Creates an `AnimationController` automatically disposed. | +| [useAnimation](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useAnimation.html) | Subscribes to an `Animation` and return its value. | #### Listenable related: | name | description | | ----------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------- | -| [useListenable](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useListenable.html) | Subscribes to a [Listenable] and mark the widget as needing build whenever the listener is called. | -| [useValueNotifier](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useValueNotifier.html) | Creates a [ValueNotifier] automatically disposed. | -| [useValueListenable](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useValueListenable.html) | Subscribes to a [ValueListenable] and return its value. | +| [useListenable](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useListenable.html) | Subscribes to a `Listenable` and mark the widget as needing build whenever the listener is called. | +| [useValueNotifier](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useValueNotifier.html) | Creates a `ValueNotifier` automatically disposed. | +| [useValueListenable](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useValueListenable.html) | Subscribes to a `ValueListenable` and return its value. | -### Misc +#### Form related: + +| name | description | +| ---- | ----------- | +| foo | bar | + +#### Misc A series of hooks with no particular theme. | name | description | | ------------------------------------------------------------------------------------------------------- | ----------------------------------------------------- | -| [useReducer](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useReducer.html) | An alternative to [useState] for more complex states. | +| [useReducer](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useReducer.html) | An alternative to `useState` for more complex states. | From 6455711ac51ab2de7f7ce7c10a1425ab71b0e6c0 Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Wed, 10 Apr 2019 20:13:04 +0200 Subject: [PATCH 064/384] fixes a crash in release build. (#68) closes #66 --- lib/src/framework.dart | 59 +++++++++++++++++++++----------------- test/hook_widget_test.dart | 19 ++++++++++++ 2 files changed, 51 insertions(+), 27 deletions(-) diff --git a/lib/src/framework.dart b/lib/src/framework.dart index 000cfc74..d90e31de 100644 --- a/lib/src/framework.dart +++ b/lib/src/framework.dart @@ -2,6 +2,11 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/scheduler.dart'; import 'package:flutter/widgets.dart'; +/// Wether to behave like in release mode or allow hot-reload for hooks. +/// +/// `true` by default. It has no impact on release builds. +bool debugHotRelaadHooksEnabled = true; + /// [Hook] is similar to a [StatelessWidget], but is not associated /// to an [Element]. /// @@ -123,8 +128,11 @@ abstract class Hook { /// /// See [Hook] for more explanations. static R use(Hook hook) { - assert(HookElement._currentContext != null, - '`Hook.use` can only be called from the build method of HookWidget'); + assert(HookElement._currentContext != null, ''' +`Hook.use` can only be called from the build method of HookWidget. +Hooks should only be called within the build method of a widget. +Calling them outside of build method leads to an unstable state and is therefore prohibited. +'''); return HookElement._currentContext._use(hook); } @@ -235,20 +243,19 @@ abstract class HookState> { /// An [Element] that uses a [HookWidget] as its configuration. class HookElement extends StatefulElement { + static HookElement _currentContext; + /// Creates an element that uses the given widget as its configuration. HookElement(HookWidget widget) : super(widget); Iterator _currentHook; int _hookIndex; List _hooks; + bool _didFinishBuildOnce = false; - bool _debugIsBuilding; - bool _didReassemble; - bool _didNotFinishBuildOnce; + bool _debugDidReassemble; bool _debugShouldDispose; - static HookElement _currentContext; - @override HookWidget get widget => super.widget as HookWidget; @@ -260,9 +267,7 @@ class HookElement extends StatefulElement { _hookIndex = 0; assert(() { _debugShouldDispose = false; - _didNotFinishBuildOnce ??= true; - _didReassemble ??= false; - _debugIsBuilding = true; + _debugDidReassemble ??= false; return true; }()); HookElement._currentContext = this; @@ -271,7 +276,8 @@ class HookElement extends StatefulElement { // dispose removed items assert(() { - if (_didReassemble && _hooks != null) { + if (!debugHotRelaadHooksEnabled) return true; + if (_debugDidReassemble && _hooks != null) { for (var i = _hookIndex; i < _hooks.length;) { _hooks.removeAt(i).dispose(); } @@ -285,11 +291,11 @@ This may happen if the call to `Hook.use` is made under some condition. '''); assert(() { - _didNotFinishBuildOnce = false; - _didReassemble = false; - _debugIsBuilding = false; + if (!debugHotRelaadHooksEnabled) return true; + _debugDidReassemble = false; return true; }()); + _didFinishBuildOnce = true; return result; } @@ -342,31 +348,30 @@ This may happen if the call to `Hook.use` is made under some condition. @override void reassemble() { super.reassemble(); - _didReassemble = true; - if (_hooks != null) { - for (final hook in _hooks) { - hook.reassemble(); + assert(() { + _debugDidReassemble = true; + if (_hooks != null) { + for (final hook in _hooks) { + hook.reassemble(); + } } - } + return true; + }()); } R _use(Hook hook) { - assert(_debugIsBuilding == true, ''' - Hooks should only be called within the build method of a widget. - Calling them outside of build method leads to an unstable state and is therefore prohibited - '''); - HookState> hookState; // first build if (_currentHook == null) { - assert(_didReassemble || _didNotFinishBuildOnce); + assert(_debugDidReassemble || !_didFinishBuildOnce); hookState = _createHookState(hook); _hooks ??= []; _hooks.add(hookState); } else { // recreate states on hot-reload of the order changed assert(() { - if (!_didReassemble) { + if (!debugHotRelaadHooksEnabled) return true; + if (!_debugDidReassemble) { return true; } if (!_debugShouldDispose && @@ -386,7 +391,7 @@ This may happen if the call to `Hook.use` is made under some condition. } return true; }()); - if (_didNotFinishBuildOnce && _currentHook.current == null) { + if (!_didFinishBuildOnce && _currentHook.current == null) { hookState = _pushHook(hook); _currentHook.moveNext(); } else { diff --git a/test/hook_widget_test.dart b/test/hook_widget_test.dart index 92f8cfaa..0e34cb91 100644 --- a/test/hook_widget_test.dart +++ b/test/hook_widget_test.dart @@ -40,6 +40,25 @@ void main() { reset(reassemble); }); + testWidgets("release mode don't crash", (tester) async { + ValueNotifier notifier; + debugHotRelaadHooksEnabled = false; + addTearDown(() => debugHotRelaadHooksEnabled = true); + + await tester.pumpWidget(HookBuilder(builder: (_) { + notifier = useState(0); + + return Text(notifier.value.toString(), textDirection: TextDirection.ltr); + })); + + expect(find.text('0'), findsOneWidget); + + notifier.value++; + await tester.pump(); + + expect(find.text('1'), findsOneWidget); + }); + testWidgets('HookElement exposes an immutable list of hooks', (tester) async { await tester.pumpWidget(HookBuilder(builder: (_) { Hook.use(HookTest()); From 43a0dd82e0c465fad19ad6eb5ff8b36f1162b341 Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Wed, 10 Apr 2019 20:14:49 +0200 Subject: [PATCH 065/384] prevent using InheritedWidgets from initHook It is now impossible to call `inheritFromWidgetOfExactType` inside `initHook` of hooks. This forces authors to handle values updates. closes #43 --- CHANGELOG.md | 1 + example/pubspec.lock | 18 +++++++++--------- lib/src/framework.dart | 26 ++++++++++++++++++++++---- pubspec.lock | 16 ++++++++-------- pubspec.yaml | 2 +- test/hook_widget_test.dart | 38 ++++++++++++++++++++++++++++++++++++++ 6 files changed, 79 insertions(+), 22 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2f773a28..3ac97691 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ ## 0.3.0: +- NEW: it is now impossible to call `inheritFromWidgetOfExactType` inside `initHook` of hooks. This forces authors to handle values updates. - FIX: use List instead of List for keys. This fixes `implicit-dynamic` rule mistakenly reporting errors. - NEW: Hooks are now visible on `HookElement` through `debugHooks` in development, for testing purposes. - NEW: If a widget throws on the first build or after a hot-reload, next rebuilds can still add/edit hooks until one `build` finishes entirely. diff --git a/example/pubspec.lock b/example/pubspec.lock index f7c87eb3..36fc8ee8 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -7,7 +7,7 @@ packages: name: async url: "https://pub.dartlang.org" source: hosted - version: "2.0.8" + version: "2.1.0" boolean_selector: dependency: transitive description: @@ -40,7 +40,7 @@ packages: path: ".." relative: true source: path - version: "0.3.0-dev.4" + version: "0.3.1-dev" flutter_test: dependency: "direct dev" description: flutter @@ -52,7 +52,7 @@ packages: name: matcher url: "https://pub.dartlang.org" source: hosted - version: "0.12.3+1" + version: "0.12.5" meta: dependency: transitive description: @@ -73,14 +73,14 @@ packages: name: pedantic url: "https://pub.dartlang.org" source: hosted - version: "1.4.0" + version: "1.5.0" quiver: dependency: transitive description: name: quiver url: "https://pub.dartlang.org" source: hosted - version: "2.0.1" + version: "2.0.2" shared_preferences: dependency: "direct main" description: @@ -99,7 +99,7 @@ packages: name: source_span url: "https://pub.dartlang.org" source: hosted - version: "1.5.4" + version: "1.5.5" stack_trace: dependency: transitive description: @@ -113,7 +113,7 @@ packages: name: stream_channel url: "https://pub.dartlang.org" source: hosted - version: "1.6.8" + version: "2.0.0" string_scanner: dependency: transitive description: @@ -134,7 +134,7 @@ packages: name: test_api url: "https://pub.dartlang.org" source: hosted - version: "0.2.2" + version: "0.2.4" typed_data: dependency: transitive description: @@ -150,5 +150,5 @@ packages: source: hosted version: "2.0.8" sdks: - dart: ">=2.1.0 <3.0.0" + dart: ">=2.2.0 <3.0.0" flutter: ">=0.1.4 <2.0.0" diff --git a/lib/src/framework.dart b/lib/src/framework.dart index 000cfc74..0ded656c 100644 --- a/lib/src/framework.dart +++ b/lib/src/framework.dart @@ -246,6 +246,7 @@ class HookElement extends StatefulElement { bool _didReassemble; bool _didNotFinishBuildOnce; bool _debugShouldDispose; + bool _debugIsInitHook; static HookElement _currentContext; @@ -263,6 +264,7 @@ class HookElement extends StatefulElement { _didNotFinishBuildOnce ??= true; _didReassemble ??= false; _debugIsBuilding = true; + _debugIsInitHook = false; return true; }()); HookElement._currentContext = this; @@ -297,8 +299,13 @@ This may happen if the call to `Hook.use` is made under some condition. /// /// These should not be used directly and are exposed @visibleForTesting - List get debugHooks { - return List.unmodifiable(_hooks); + List get debugHooks => List.unmodifiable(_hooks); + + @override + InheritedWidget inheritFromWidgetOfExactType(Type targetType, + {Object aspect}) { + assert(!_debugIsInitHook); + return super.inheritFromWidgetOfExactType(targetType, aspect: aspect); } @override @@ -449,10 +456,21 @@ This may happen if the call to `Hook.use` is made under some condition. } HookState> _createHookState(Hook hook) { - return hook.createState() - .._element = state + assert(() { + _debugIsInitHook = true; + return true; + }()); + final state = hook.createState() + .._element = this.state .._hook = hook ..initHook(); + + assert(() { + _debugIsInitHook = false; + return true; + }()); + + return state; } } diff --git a/pubspec.lock b/pubspec.lock index 08cbecef..496acba8 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -7,7 +7,7 @@ packages: name: async url: "https://pub.dartlang.org" source: hosted - version: "2.0.8" + version: "2.1.0" boolean_selector: dependency: transitive description: @@ -45,7 +45,7 @@ packages: name: matcher url: "https://pub.dartlang.org" source: hosted - version: "0.12.3+1" + version: "0.12.5" meta: dependency: transitive description: @@ -73,14 +73,14 @@ packages: name: pedantic url: "https://pub.dartlang.org" source: hosted - version: "1.4.0" + version: "1.5.0" quiver: dependency: transitive description: name: quiver url: "https://pub.dartlang.org" source: hosted - version: "2.0.1" + version: "2.0.2" sky_engine: dependency: transitive description: flutter @@ -92,7 +92,7 @@ packages: name: source_span url: "https://pub.dartlang.org" source: hosted - version: "1.5.4" + version: "1.5.5" stack_trace: dependency: transitive description: @@ -106,7 +106,7 @@ packages: name: stream_channel url: "https://pub.dartlang.org" source: hosted - version: "1.6.8" + version: "2.0.0" string_scanner: dependency: transitive description: @@ -127,7 +127,7 @@ packages: name: test_api url: "https://pub.dartlang.org" source: hosted - version: "0.2.2" + version: "0.2.4" typed_data: dependency: transitive description: @@ -143,4 +143,4 @@ packages: source: hosted version: "2.0.8" sdks: - dart: ">=2.1.0 <3.0.0" + dart: ">=2.2.0 <3.0.0" diff --git a/pubspec.yaml b/pubspec.yaml index 2f5d8652..f2b979c4 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -3,7 +3,7 @@ description: A flutter implementation of React hooks. It adds a new kind of widg homepage: https://github.com/rrousselGit/flutter_hooks author: Remi Rousselet -version: 0.3.0-dev.4 +version: 0.3.1-dev environment: sdk: ">=2.0.0-dev.68.0 <3.0.0" diff --git a/test/hook_widget_test.dart b/test/hook_widget_test.dart index 92f8cfaa..bebb52ca 100644 --- a/test/hook_widget_test.dart +++ b/test/hook_widget_test.dart @@ -4,6 +4,21 @@ import 'package:flutter_hooks/flutter_hooks.dart'; import 'mock.dart'; +class InheritedInitHook extends Hook { + @override + InheritedInitHookState createState() => InheritedInitHookState(); +} + +class InheritedInitHookState extends HookState { + @override + void initHook() { + context.inheritFromWidgetOfExactType(InheritedWidget); + } + + @override + void build(BuildContext context) {} +} + void main() { final build = Func1(); final dispose = Func0(); @@ -40,6 +55,29 @@ void main() { reset(reassemble); }); + testWidgets('should not allow using inheritedwidgets inside initHook', + (tester) async { + await tester.pumpWidget(HookBuilder(builder: (_) { + Hook.use(InheritedInitHook()); + return Container(); + })); + + expect(tester.takeException(), isAssertionError); + }); + + testWidgets('allows using inherited widgets outside of initHook', + (tester) async { + when(build(any)).thenAnswer((invocation) { + invocation.positionalArguments.first as BuildContext + ..inheritFromWidgetOfExactType(InheritedWidget); + }); + + await tester.pumpWidget(HookBuilder(builder: (_) { + Hook.use(HookTest(build: build)); + return Container(); + })); + }); + testWidgets('HookElement exposes an immutable list of hooks', (tester) async { await tester.pumpWidget(HookBuilder(builder: (_) { Hook.use(HookTest()); From 0193e583574646e49008175f292993664cb49a84 Mon Sep 17 00:00:00 2001 From: Emrah Bilbay Date: Wed, 10 Apr 2019 21:19:12 +0300 Subject: [PATCH 066/384] fix typo --- lib/src/framework.dart | 8 ++++---- test/hook_widget_test.dart | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/src/framework.dart b/lib/src/framework.dart index d90e31de..e323d776 100644 --- a/lib/src/framework.dart +++ b/lib/src/framework.dart @@ -5,7 +5,7 @@ import 'package:flutter/widgets.dart'; /// Wether to behave like in release mode or allow hot-reload for hooks. /// /// `true` by default. It has no impact on release builds. -bool debugHotRelaadHooksEnabled = true; +bool debugHotReloadHooksEnabled = true; /// [Hook] is similar to a [StatelessWidget], but is not associated /// to an [Element]. @@ -276,7 +276,7 @@ class HookElement extends StatefulElement { // dispose removed items assert(() { - if (!debugHotRelaadHooksEnabled) return true; + if (!debugHotReloadHooksEnabled) return true; if (_debugDidReassemble && _hooks != null) { for (var i = _hookIndex; i < _hooks.length;) { _hooks.removeAt(i).dispose(); @@ -291,7 +291,7 @@ This may happen if the call to `Hook.use` is made under some condition. '''); assert(() { - if (!debugHotRelaadHooksEnabled) return true; + if (!debugHotReloadHooksEnabled) return true; _debugDidReassemble = false; return true; }()); @@ -370,7 +370,7 @@ This may happen if the call to `Hook.use` is made under some condition. } else { // recreate states on hot-reload of the order changed assert(() { - if (!debugHotRelaadHooksEnabled) return true; + if (!debugHotReloadHooksEnabled) return true; if (!_debugDidReassemble) { return true; } diff --git a/test/hook_widget_test.dart b/test/hook_widget_test.dart index 0e34cb91..eaa58f45 100644 --- a/test/hook_widget_test.dart +++ b/test/hook_widget_test.dart @@ -42,8 +42,8 @@ void main() { testWidgets("release mode don't crash", (tester) async { ValueNotifier notifier; - debugHotRelaadHooksEnabled = false; - addTearDown(() => debugHotRelaadHooksEnabled = true); + debugHotReloadHooksEnabled = false; + addTearDown(() => debugHotReloadHooksEnabled = true); await tester.pumpWidget(HookBuilder(builder: (_) { notifier = useState(0); From 57f1d65aca4db49365c6dffed4cd36c57584fdb3 Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Sat, 20 Apr 2019 15:50:47 +0200 Subject: [PATCH 067/384] fix readme --- README.md | 6 ------ 1 file changed, 6 deletions(-) diff --git a/README.md b/README.md index de620201..6dd7ce76 100644 --- a/README.md +++ b/README.md @@ -324,12 +324,6 @@ They will take care of creating/updating/disposing an object. | [useValueNotifier](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useValueNotifier.html) | Creates a `ValueNotifier` automatically disposed. | | [useValueListenable](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useValueListenable.html) | Subscribes to a `ValueListenable` and return its value. | -#### Form related: - -| name | description | -| ---- | ----------- | -| foo | bar | - #### Misc A series of hooks with no particular theme. From e347deca31aa8f9264f93a8385b2033d101fc1aa Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Sat, 20 Apr 2019 16:16:18 +0200 Subject: [PATCH 068/384] new: usePrevious --- CHANGELOG.md | 1 + README.md | 7 ++++--- example/pubspec.lock | 18 +++++++++--------- lib/src/misc.dart | 26 ++++++++++++++++++++++++++ pubspec.lock | 16 ++++++++-------- pubspec.yaml | 4 ++-- test/use_previous_test.dart | 33 +++++++++++++++++++++++++++++++++ 7 files changed, 83 insertions(+), 22 deletions(-) create mode 100644 test/use_previous_test.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index 3ac97691..9405f6c1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ ## 0.3.0: +- NEW: `usePrevious`, a hook that returns the previous argument it received. - NEW: it is now impossible to call `inheritFromWidgetOfExactType` inside `initHook` of hooks. This forces authors to handle values updates. - FIX: use List instead of List for keys. This fixes `implicit-dynamic` rule mistakenly reporting errors. - NEW: Hooks are now visible on `HookElement` through `debugHooks` in development, for testing purposes. diff --git a/README.md b/README.md index 6dd7ce76..b4931394 100644 --- a/README.md +++ b/README.md @@ -328,6 +328,7 @@ They will take care of creating/updating/disposing an object. A series of hooks with no particular theme. -| name | description | -| ------------------------------------------------------------------------------------------------------- | ----------------------------------------------------- | -| [useReducer](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useReducer.html) | An alternative to `useState` for more complex states. | +| name | description | +| --------------------------------------------------------------------------------------------------------- | ------------------------------------------------------ | +| [useReducer](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useReducer.html) | An alternative to `useState` for more complex states. | +| [usePrevious](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/usePrevious.html) | Returns the previous argument called to [usePrevious]. | diff --git a/example/pubspec.lock b/example/pubspec.lock index 36fc8ee8..fcb384f3 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -7,7 +7,7 @@ packages: name: async url: "https://pub.dartlang.org" source: hosted - version: "2.1.0" + version: "2.0.8" boolean_selector: dependency: transitive description: @@ -40,7 +40,7 @@ packages: path: ".." relative: true source: path - version: "0.3.1-dev" + version: "0.3.0" flutter_test: dependency: "direct dev" description: flutter @@ -52,7 +52,7 @@ packages: name: matcher url: "https://pub.dartlang.org" source: hosted - version: "0.12.5" + version: "0.12.3+1" meta: dependency: transitive description: @@ -73,14 +73,14 @@ packages: name: pedantic url: "https://pub.dartlang.org" source: hosted - version: "1.5.0" + version: "1.4.0" quiver: dependency: transitive description: name: quiver url: "https://pub.dartlang.org" source: hosted - version: "2.0.2" + version: "2.0.1" shared_preferences: dependency: "direct main" description: @@ -99,7 +99,7 @@ packages: name: source_span url: "https://pub.dartlang.org" source: hosted - version: "1.5.5" + version: "1.5.4" stack_trace: dependency: transitive description: @@ -113,7 +113,7 @@ packages: name: stream_channel url: "https://pub.dartlang.org" source: hosted - version: "2.0.0" + version: "1.6.8" string_scanner: dependency: transitive description: @@ -134,7 +134,7 @@ packages: name: test_api url: "https://pub.dartlang.org" source: hosted - version: "0.2.4" + version: "0.2.2" typed_data: dependency: transitive description: @@ -150,5 +150,5 @@ packages: source: hosted version: "2.0.8" sdks: - dart: ">=2.2.0 <3.0.0" + dart: ">=2.1.0 <3.0.0" flutter: ">=0.1.4 <2.0.0" diff --git a/lib/src/misc.dart b/lib/src/misc.dart index 3cc636c0..3e10f1c8 100644 --- a/lib/src/misc.dart +++ b/lib/src/misc.dart @@ -84,3 +84,29 @@ class _ReducerdHookState return this; } } + +/// Returns the previous argument called to [usePrevious]. +T usePrevious(T val) { + return Hook.use(_PreviousHook(val)); +} + +class _PreviousHook extends Hook { + _PreviousHook(this.value); + + final T value; + + @override + _PreviousHookState createState() => _PreviousHookState(); +} + +class _PreviousHookState extends HookState> { + T previous; + + @override + void didUpdateHook(_PreviousHook old) { + previous = old.value; + } + + @override + T build(BuildContext context) => previous; +} diff --git a/pubspec.lock b/pubspec.lock index 496acba8..08cbecef 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -7,7 +7,7 @@ packages: name: async url: "https://pub.dartlang.org" source: hosted - version: "2.1.0" + version: "2.0.8" boolean_selector: dependency: transitive description: @@ -45,7 +45,7 @@ packages: name: matcher url: "https://pub.dartlang.org" source: hosted - version: "0.12.5" + version: "0.12.3+1" meta: dependency: transitive description: @@ -73,14 +73,14 @@ packages: name: pedantic url: "https://pub.dartlang.org" source: hosted - version: "1.5.0" + version: "1.4.0" quiver: dependency: transitive description: name: quiver url: "https://pub.dartlang.org" source: hosted - version: "2.0.2" + version: "2.0.1" sky_engine: dependency: transitive description: flutter @@ -92,7 +92,7 @@ packages: name: source_span url: "https://pub.dartlang.org" source: hosted - version: "1.5.5" + version: "1.5.4" stack_trace: dependency: transitive description: @@ -106,7 +106,7 @@ packages: name: stream_channel url: "https://pub.dartlang.org" source: hosted - version: "2.0.0" + version: "1.6.8" string_scanner: dependency: transitive description: @@ -127,7 +127,7 @@ packages: name: test_api url: "https://pub.dartlang.org" source: hosted - version: "0.2.4" + version: "0.2.2" typed_data: dependency: transitive description: @@ -143,4 +143,4 @@ packages: source: hosted version: "2.0.8" sdks: - dart: ">=2.2.0 <3.0.0" + dart: ">=2.1.0 <3.0.0" diff --git a/pubspec.yaml b/pubspec.yaml index f2b979c4..3b541b7d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -3,10 +3,10 @@ description: A flutter implementation of React hooks. It adds a new kind of widg homepage: https://github.com/rrousselGit/flutter_hooks author: Remi Rousselet -version: 0.3.1-dev +version: 0.3.0 environment: - sdk: ">=2.0.0-dev.68.0 <3.0.0" + sdk: ">=2.0.0 <3.0.0" dependencies: flutter: diff --git a/test/use_previous_test.dart b/test/use_previous_test.dart new file mode 100644 index 00000000..0738fdfa --- /dev/null +++ b/test/use_previous_test.dart @@ -0,0 +1,33 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:flutter_test/flutter_test.dart'; + +Widget build(int value) => HookBuilder( + builder: (context) => + Text(usePrevious(value).toString(), textDirection: TextDirection.ltr), + ); +void main() { + group('usePrevious', () { + testWidgets('default value is null', (tester) async { + await tester.pumpWidget(build(0)); + + expect(find.text('null'), findsOneWidget); + }); + testWidgets('subsequent build returns previous value', (tester) async { + await tester.pumpWidget(build(0)); + await tester.pumpWidget(build(1)); + + expect(find.text('0'), findsOneWidget); + + await tester.pumpWidget(build(1)); + + expect(find.text('1'), findsOneWidget); + + await tester.pumpWidget(build(2)); + expect(find.text('1'), findsOneWidget); + + await tester.pumpWidget(build(3)); + expect(find.text('2'), findsOneWidget); + }); + }); +} From 3f068a5dbbdc22253830eff7b6b2712279142fc4 Mon Sep 17 00:00:00 2001 From: Felix Angelov Date: Wed, 1 May 2019 21:54:16 -0500 Subject: [PATCH 069/384] remove calls to super.lifecycle from existing hooks --- README.md | 3 --- lib/src/animation.dart | 2 -- lib/src/async.dart | 3 --- lib/src/framework.dart | 2 -- lib/src/listenable.dart | 2 -- lib/src/primitives.dart | 2 -- test/mock.dart | 1 - 7 files changed, 15 deletions(-) diff --git a/README.md b/README.md index b4931394..828caf3d 100644 --- a/README.md +++ b/README.md @@ -29,13 +29,11 @@ class _ExampleState extends State with SingleTickerProviderStateMixin { @override void initState() { - super.initState(); _controller = AnimationController(vsync: this, duration: widget.duration); } @override void didUpdateWidget(Example oldWidget) { - super.didUpdateWidget(oldWidget); if (widget.duration != oldWidget.duration) { _controller.duration = widget.duration; } @@ -43,7 +41,6 @@ class _ExampleState extends State with SingleTickerProviderStateMixin { @override void dispose() { - super.dispose(); _controller.dispose(); } diff --git a/lib/src/animation.dart b/lib/src/animation.dart index 37ba148d..3c32126a 100644 --- a/lib/src/animation.dart +++ b/lib/src/animation.dart @@ -108,7 +108,6 @@ Switching between controller and uncontrolled vsync is not allowed. @override void dispose() { - super.dispose(); _animationController.dispose(); } } @@ -162,7 +161,6 @@ class _TickerProviderHookState ' by AnimationControllers should be disposed by calling dispose() on ' ' the AnimationController itself. Otherwise, the ticker will leak.\n'); }()); - super.dispose(); } @override diff --git a/lib/src/async.dart b/lib/src/async.dart index e945815a..fb1a1a53 100644 --- a/lib/src/async.dart +++ b/lib/src/async.dart @@ -60,7 +60,6 @@ class _FutureStateHook extends HookState, _FutureHook> { @override void dispose() { _unsubscribe(); - super.dispose(); } void _subscribe() { @@ -150,7 +149,6 @@ class _StreamHookState extends HookState, _StreamHook> { @override void dispose() { _unsubscribe(); - super.dispose(); } void _subscribe() { @@ -270,6 +268,5 @@ class _StreamControllerHookState @override void dispose() { _controller.close(); - super.dispose(); } } diff --git a/lib/src/framework.dart b/lib/src/framework.dart index 708d3b85..dcc1ecb3 100644 --- a/lib/src/framework.dart +++ b/lib/src/framework.dart @@ -71,7 +71,6 @@ bool debugHotReloadHooksEnabled = true; /// /// @override /// void initState() { -/// super.initState(); /// _controller = AnimationController( /// vsync: this, /// duration: const Duration(seconds: 1), @@ -80,7 +79,6 @@ bool debugHotReloadHooksEnabled = true; /// /// @override /// void dispose() { -/// super.dispose(); /// _controller.dispose(); /// } /// diff --git a/lib/src/listenable.dart b/lib/src/listenable.dart index 8a2ae3bd..b80db47f 100644 --- a/lib/src/listenable.dart +++ b/lib/src/listenable.dart @@ -54,7 +54,6 @@ class _ListenableStateHook extends HookState { @override void dispose() { - super.dispose(); hook.listenable.removeListener(_listener); } } @@ -102,6 +101,5 @@ class _UseValueNotiferHookState @override void dispose() { notifier.dispose(); - super.dispose(); } } diff --git a/lib/src/primitives.dart b/lib/src/primitives.dart index a2a53328..2b1e4b42 100644 --- a/lib/src/primitives.dart +++ b/lib/src/primitives.dart @@ -167,7 +167,6 @@ class _EffectHookState extends HookState { if (disposer != null) { disposer(); } - super.dispose(); } void scheduleEffect() { @@ -228,7 +227,6 @@ class _StateHookState extends HookState, _StateHook> { @override void dispose() { _state.dispose(); - super.dispose(); } @override diff --git a/test/mock.dart b/test/mock.dart index 6df09d0e..e6f15107 100644 --- a/test/mock.dart +++ b/test/mock.dart @@ -61,7 +61,6 @@ class HookStateTest extends HookState> { @override void dispose() { - super.dispose(); if (hook.dispose != null) { hook.dispose(); } From f6095dae0ebc0f9af626e024e94fb3f6f4391008 Mon Sep 17 00:00:00 2001 From: Felix Angelov Date: Wed, 1 May 2019 22:17:28 -0500 Subject: [PATCH 070/384] apply feedback --- lib/src/framework.dart | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/src/framework.dart b/lib/src/framework.dart index dcc1ecb3..e6034678 100644 --- a/lib/src/framework.dart +++ b/lib/src/framework.dart @@ -71,6 +71,7 @@ bool debugHotReloadHooksEnabled = true; /// /// @override /// void initState() { +/// super.initState(); /// _controller = AnimationController( /// vsync: this, /// duration: const Duration(seconds: 1), @@ -80,6 +81,7 @@ bool debugHotReloadHooksEnabled = true; /// @override /// void dispose() { /// _controller.dispose(); +/// super.dispose(); /// } /// /// @override From be2d4b932cb5c107955f66627048d3c9b7f89a3c Mon Sep 17 00:00:00 2001 From: Felix Angelov Date: Thu, 2 May 2019 08:11:54 -0500 Subject: [PATCH 071/384] reverted additional unnecessary changes --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index 828caf3d..19c37cf9 100644 --- a/README.md +++ b/README.md @@ -29,11 +29,13 @@ class _ExampleState extends State with SingleTickerProviderStateMixin { @override void initState() { + super.initState(); _controller = AnimationController(vsync: this, duration: widget.duration); } @override void didUpdateWidget(Example oldWidget) { + super.didUpdateWidget(oldWidget); if (widget.duration != oldWidget.duration) { _controller.duration = widget.duration; } @@ -42,6 +44,7 @@ class _ExampleState extends State with SingleTickerProviderStateMixin { @override void dispose() { _controller.dispose(); + super.dispose(); } @override From 4b9a74c0a117554a2a3fdc8ba1fab8f11da477d8 Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Thu, 2 May 2019 15:45:44 +0200 Subject: [PATCH 072/384] fix hooks not compiling on recent flutter releases This solve a recent breaking change: https://groups.google.com/forum/#!topic/flutter-announce/hp1RNIgej38 closes #77 --- CHANGELOG.md | 4 ++++ example/pubspec.lock | 18 +++++++++--------- lib/src/framework.dart | 5 +++-- pubspec.lock | 17 +++++++++-------- pubspec.yaml | 1 + 5 files changed, 26 insertions(+), 19 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9405f6c1..5395233f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.4.0: + +- Make hooks compatible with newer flutter version. (see https://groups.google.com/forum/#!topic/flutter-announce/hp1RNIgej38) + ## 0.3.0: - NEW: `usePrevious`, a hook that returns the previous argument it received. diff --git a/example/pubspec.lock b/example/pubspec.lock index fcb384f3..a25ecbd2 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -7,7 +7,7 @@ packages: name: async url: "https://pub.dartlang.org" source: hosted - version: "2.0.8" + version: "2.2.0" boolean_selector: dependency: transitive description: @@ -52,7 +52,7 @@ packages: name: matcher url: "https://pub.dartlang.org" source: hosted - version: "0.12.3+1" + version: "0.12.5" meta: dependency: transitive description: @@ -73,14 +73,14 @@ packages: name: pedantic url: "https://pub.dartlang.org" source: hosted - version: "1.4.0" + version: "1.5.0" quiver: dependency: transitive description: name: quiver url: "https://pub.dartlang.org" source: hosted - version: "2.0.1" + version: "2.0.3" shared_preferences: dependency: "direct main" description: @@ -99,7 +99,7 @@ packages: name: source_span url: "https://pub.dartlang.org" source: hosted - version: "1.5.4" + version: "1.5.5" stack_trace: dependency: transitive description: @@ -113,7 +113,7 @@ packages: name: stream_channel url: "https://pub.dartlang.org" source: hosted - version: "1.6.8" + version: "2.0.0" string_scanner: dependency: transitive description: @@ -134,7 +134,7 @@ packages: name: test_api url: "https://pub.dartlang.org" source: hosted - version: "0.2.2" + version: "0.2.5" typed_data: dependency: transitive description: @@ -150,5 +150,5 @@ packages: source: hosted version: "2.0.8" sdks: - dart: ">=2.1.0 <3.0.0" - flutter: ">=0.1.4 <2.0.0" + dart: ">=2.2.0 <3.0.0" + flutter: ">=1.5.0-pre <2.0.0" diff --git a/lib/src/framework.dart b/lib/src/framework.dart index 708d3b85..79c28798 100644 --- a/lib/src/framework.dart +++ b/lib/src/framework.dart @@ -325,7 +325,8 @@ This may happen if the call to `Hook.use` is made under some condition. exception: exception, stack: stack, library: 'hooks library', - context: 'while calling `didBuild` on ${hook.runtimeType}', + context: ErrorDescription( + 'while calling `didBuild` on ${hook.runtimeType}'), )); } } @@ -345,7 +346,7 @@ This may happen if the call to `Hook.use` is made under some condition. exception: exception, stack: stack, library: 'hooks library', - context: 'while disposing ${hook.runtimeType}', + context: ErrorDescription('while disposing ${hook.runtimeType}'), )); } } diff --git a/pubspec.lock b/pubspec.lock index 08cbecef..b63b81e3 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -7,7 +7,7 @@ packages: name: async url: "https://pub.dartlang.org" source: hosted - version: "2.0.8" + version: "2.2.0" boolean_selector: dependency: transitive description: @@ -45,7 +45,7 @@ packages: name: matcher url: "https://pub.dartlang.org" source: hosted - version: "0.12.3+1" + version: "0.12.5" meta: dependency: transitive description: @@ -73,14 +73,14 @@ packages: name: pedantic url: "https://pub.dartlang.org" source: hosted - version: "1.4.0" + version: "1.5.0" quiver: dependency: transitive description: name: quiver url: "https://pub.dartlang.org" source: hosted - version: "2.0.1" + version: "2.0.3" sky_engine: dependency: transitive description: flutter @@ -92,7 +92,7 @@ packages: name: source_span url: "https://pub.dartlang.org" source: hosted - version: "1.5.4" + version: "1.5.5" stack_trace: dependency: transitive description: @@ -106,7 +106,7 @@ packages: name: stream_channel url: "https://pub.dartlang.org" source: hosted - version: "1.6.8" + version: "2.0.0" string_scanner: dependency: transitive description: @@ -127,7 +127,7 @@ packages: name: test_api url: "https://pub.dartlang.org" source: hosted - version: "0.2.2" + version: "0.2.5" typed_data: dependency: transitive description: @@ -143,4 +143,5 @@ packages: source: hosted version: "2.0.8" sdks: - dart: ">=2.1.0 <3.0.0" + dart: ">=2.2.0 <3.0.0" + flutter: ">=1.5.0-pre <=2.0.0" diff --git a/pubspec.yaml b/pubspec.yaml index 3b541b7d..0b7dcfed 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -7,6 +7,7 @@ version: 0.3.0 environment: sdk: ">=2.0.0 <3.0.0" + flutter: ">= 1.5.0-pre <= 2.0.0" dependencies: flutter: From d78ad980c15618d72e04e2f048bd97d9275510b2 Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Thu, 2 May 2019 16:09:13 +0200 Subject: [PATCH 073/384] v0.4.0 --- example/pubspec.lock | 2 +- pubspec.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/example/pubspec.lock b/example/pubspec.lock index a25ecbd2..63c43949 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -40,7 +40,7 @@ packages: path: ".." relative: true source: path - version: "0.3.0" + version: "0.4.0" flutter_test: dependency: "direct dev" description: flutter diff --git a/pubspec.yaml b/pubspec.yaml index 0b7dcfed..6feb5b56 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -3,7 +3,7 @@ description: A flutter implementation of React hooks. It adds a new kind of widg homepage: https://github.com/rrousselGit/flutter_hooks author: Remi Rousselet -version: 0.3.0 +version: 0.4.0 environment: sdk: ">=2.0.0 <3.0.0" From 63293fbaf780348c15450855513e5815cd59fa4a Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Mon, 13 May 2019 14:54:09 +0200 Subject: [PATCH 074/384] Revert "fix hooks not compiling on recent flutter releases" (#85) * Revert "fix hooks not compiling" * run ci on stable --- .travis.yml | 2 +- example/pubspec.lock | 18 +++++++++--------- lib/src/framework.dart | 5 ++--- pubspec.yaml | 2 +- 4 files changed, 13 insertions(+), 14 deletions(-) diff --git a/.travis.yml b/.travis.yml index 22230988..f30382f3 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,7 +4,7 @@ os: sudo: false before_script: - cd .. - - git clone https://github.com/flutter/flutter.git -b master + - git clone https://github.com/flutter/flutter.git -b stable - export PATH=$PATH:$PWD/flutter/bin - export PATH=$PATH:$PWD/flutter/bin/cache/dart-sdk/bin - flutter doctor diff --git a/example/pubspec.lock b/example/pubspec.lock index 63c43949..01000bce 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -7,7 +7,7 @@ packages: name: async url: "https://pub.dartlang.org" source: hosted - version: "2.2.0" + version: "2.0.8" boolean_selector: dependency: transitive description: @@ -52,7 +52,7 @@ packages: name: matcher url: "https://pub.dartlang.org" source: hosted - version: "0.12.5" + version: "0.12.3+1" meta: dependency: transitive description: @@ -73,14 +73,14 @@ packages: name: pedantic url: "https://pub.dartlang.org" source: hosted - version: "1.5.0" + version: "1.4.0" quiver: dependency: transitive description: name: quiver url: "https://pub.dartlang.org" source: hosted - version: "2.0.3" + version: "2.0.1" shared_preferences: dependency: "direct main" description: @@ -99,7 +99,7 @@ packages: name: source_span url: "https://pub.dartlang.org" source: hosted - version: "1.5.5" + version: "1.5.4" stack_trace: dependency: transitive description: @@ -113,7 +113,7 @@ packages: name: stream_channel url: "https://pub.dartlang.org" source: hosted - version: "2.0.0" + version: "1.6.8" string_scanner: dependency: transitive description: @@ -134,7 +134,7 @@ packages: name: test_api url: "https://pub.dartlang.org" source: hosted - version: "0.2.5" + version: "0.2.2" typed_data: dependency: transitive description: @@ -150,5 +150,5 @@ packages: source: hosted version: "2.0.8" sdks: - dart: ">=2.2.0 <3.0.0" - flutter: ">=1.5.0-pre <2.0.0" + dart: ">=2.1.0 <3.0.0" + flutter: ">=0.1.4 <2.0.0" diff --git a/lib/src/framework.dart b/lib/src/framework.dart index 4284b5a9..e6034678 100644 --- a/lib/src/framework.dart +++ b/lib/src/framework.dart @@ -325,8 +325,7 @@ This may happen if the call to `Hook.use` is made under some condition. exception: exception, stack: stack, library: 'hooks library', - context: ErrorDescription( - 'while calling `didBuild` on ${hook.runtimeType}'), + context: 'while calling `didBuild` on ${hook.runtimeType}', )); } } @@ -346,7 +345,7 @@ This may happen if the call to `Hook.use` is made under some condition. exception: exception, stack: stack, library: 'hooks library', - context: ErrorDescription('while disposing ${hook.runtimeType}'), + context: 'while disposing ${hook.runtimeType}', )); } } diff --git a/pubspec.yaml b/pubspec.yaml index 6feb5b56..049ae98d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -7,7 +7,7 @@ version: 0.4.0 environment: sdk: ">=2.0.0 <3.0.0" - flutter: ">= 1.5.0-pre <= 2.0.0" + flutter: ">=1.0.0 < 1.5.8" dependencies: flutter: From a3e52eac2bcfac7fc75284f6e22a4a5dff381714 Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Mon, 13 May 2019 15:09:14 +0200 Subject: [PATCH 075/384] bump minor --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index 049ae98d..6ed457e6 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -3,7 +3,7 @@ description: A flutter implementation of React hooks. It adds a new kind of widg homepage: https://github.com/rrousselGit/flutter_hooks author: Remi Rousselet -version: 0.4.0 +version: 0.5.0 environment: sdk: ">=2.0.0 <3.0.0" From 818503e2fe19e04f5fe45da7ecb41141fff6e146 Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Wed, 10 Jul 2019 15:09:58 +0200 Subject: [PATCH 076/384] Stable (#94) Update flutter_hooks to Flutter 1.7.8-hotfix.2 --- CHANGELOG.md | 4 +++ example/lib/use_effect.dart | 1 + lib/src/framework.dart | 6 ++-- pubspec.lock | 8 ++--- pubspec.yaml | 4 +-- test/hook_widget_test.dart | 5 +-- test/use_animation_controller_test.dart | 20 +++++------ test/use_effect_test.dart | 6 ++-- test/use_reducer_test.dart | 48 ++++++++++++------------- 9 files changed, 55 insertions(+), 47 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5395233f..8dd94b75 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.6.0: + +- Make hooks compatible with newer flutter stable version 1.7.8-hotfix.2. + ## 0.4.0: - Make hooks compatible with newer flutter version. (see https://groups.google.com/forum/#!topic/flutter-announce/hp1RNIgej38) diff --git a/example/lib/use_effect.dart b/example/lib/use_effect.dart index a9c2951f..194d08d8 100644 --- a/example/lib/use_effect.dart +++ b/example/lib/use_effect.dart @@ -79,6 +79,7 @@ StreamController _useLocalStorageInt( int valueFromStorage = prefs.getInt(key); controller.add(valueFromStorage ?? defaultValue); }).catchError(controller.addError); + return null; }, // Pass the controller and key to the useEffect hook. This will ensure the // useEffect hook is only called the first build or when one of the the diff --git a/lib/src/framework.dart b/lib/src/framework.dart index e6034678..7a09d990 100644 --- a/lib/src/framework.dart +++ b/lib/src/framework.dart @@ -325,7 +325,8 @@ This may happen if the call to `Hook.use` is made under some condition. exception: exception, stack: stack, library: 'hooks library', - context: 'while calling `didBuild` on ${hook.runtimeType}', + context: DiagnosticsNode.message( + 'while calling `didBuild` on ${hook.runtimeType}'), )); } } @@ -345,7 +346,8 @@ This may happen if the call to `Hook.use` is made under some condition. exception: exception, stack: stack, library: 'hooks library', - context: 'while disposing ${hook.runtimeType}', + context: + DiagnosticsNode.message('while disposing ${hook.runtimeType}'), )); } } diff --git a/pubspec.lock b/pubspec.lock index b63b81e3..53bffccd 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1,5 +1,5 @@ # Generated by pub -# See https://www.dartlang.org/tools/pub/glossary#lockfile +# See https://dart.dev/tools/pub/glossary#lockfile packages: async: dependency: transitive @@ -73,7 +73,7 @@ packages: name: pedantic url: "https://pub.dartlang.org" source: hosted - version: "1.5.0" + version: "1.7.0" quiver: dependency: transitive description: @@ -143,5 +143,5 @@ packages: source: hosted version: "2.0.8" sdks: - dart: ">=2.2.0 <3.0.0" - flutter: ">=1.5.0-pre <=2.0.0" + dart: ">=2.2.2 <3.0.0" + flutter: ">=1.5.8 <2.0.0" diff --git a/pubspec.yaml b/pubspec.yaml index 6ed457e6..9715873c 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -3,11 +3,11 @@ description: A flutter implementation of React hooks. It adds a new kind of widg homepage: https://github.com/rrousselGit/flutter_hooks author: Remi Rousselet -version: 0.5.0 +version: 0.6.0 environment: sdk: ">=2.0.0 <3.0.0" - flutter: ">=1.0.0 < 1.5.8" + flutter: ">=1.5.8 <2.0.0" dependencies: flutter: diff --git a/test/hook_widget_test.dart b/test/hook_widget_test.dart index 02ff82f7..5f91aec5 100644 --- a/test/hook_widget_test.dart +++ b/test/hook_widget_test.dart @@ -70,6 +70,7 @@ void main() { when(build(any)).thenAnswer((invocation) { invocation.positionalArguments.first as BuildContext ..inheritFromWidgetOfExactType(InheritedWidget); + return null; }); await tester.pumpWidget(HookBuilder(builder: (_) { @@ -424,8 +425,8 @@ void main() { await expectPump( () => tester.pumpWidget(HookBuilder( - builder: builder.call, - )), + builder: builder.call, + )), throwsA(42), ); diff --git a/test/use_animation_controller_test.dart b/test/use_animation_controller_test.dart index 42822df7..fd078c7c 100644 --- a/test/use_animation_controller_test.dart +++ b/test/use_animation_controller_test.dart @@ -115,11 +115,11 @@ void main() { await expectPump( () => tester.pumpWidget(HookBuilder( - builder: (context) { - useAnimationController(vsync: tester); - return Container(); - }, - )), + builder: (context) { + useAnimationController(vsync: tester); + return Container(); + }, + )), throwsAssertionError, ); @@ -135,11 +135,11 @@ void main() { await expectPump( () => tester.pumpWidget(HookBuilder( - builder: (context) { - useAnimationController(); - return Container(); - }, - )), + builder: (context) { + useAnimationController(); + return Container(); + }, + )), throwsAssertionError, ); }); diff --git a/test/use_effect_test.dart b/test/use_effect_test.dart index fa556859..49d9ef1f 100644 --- a/test/use_effect_test.dart +++ b/test/use_effect_test.dart @@ -22,9 +22,9 @@ void main() { testWidgets('useEffect null callback throws', (tester) async { await expectPump( () => tester.pumpWidget(HookBuilder(builder: (c) { - useEffect(null); - return Container(); - })), + useEffect(null); + return Container(); + })), throwsAssertionError, ); }); diff --git a/test/use_reducer_test.dart b/test/use_reducer_test.dart index 6bf88069..78f3b81f 100644 --- a/test/use_reducer_test.dart +++ b/test/use_reducer_test.dart @@ -51,11 +51,11 @@ void main() { testWidgets('reducer required', (tester) async { await expectPump( () => tester.pumpWidget(HookBuilder( - builder: (context) { - useReducer(null); - return Container(); - }, - )), + builder: (context) { + useReducer(null); + return Container(); + }, + )), throwsAssertionError, ); }); @@ -65,11 +65,11 @@ void main() { await expectPump( () => tester.pumpWidget(HookBuilder( - builder: (context) { - useReducer(reducer.call).dispatch('Foo'); - return Container(); - }, - )), + builder: (context) { + useReducer(reducer.call).dispatch('Foo'); + return Container(); + }, + )), throwsAssertionError, ); }); @@ -80,15 +80,15 @@ void main() { when(reducer.call(0, 'Foo')).thenReturn(0); await expectPump( () => tester.pumpWidget(HookBuilder( - builder: (context) { - useReducer( - reducer.call, - initialAction: 'Foo', - initialState: 0, - ); - return Container(); - }, - )), + builder: (context) { + useReducer( + reducer.call, + initialAction: 'Foo', + initialState: 0, + ); + return Container(); + }, + )), completes, ); }); @@ -119,11 +119,11 @@ void main() { await expectPump( () => tester.pumpWidget(HookBuilder( - builder: (context) { - useReducer(reducer.call); - return Container(); - }, - )), + builder: (context) { + useReducer(reducer.call); + return Container(); + }, + )), throwsAssertionError, ); }); From f2ca830a52d80951bc70cd4ee1c1b12e89979b90 Mon Sep 17 00:00:00 2001 From: Adam Patridge Date: Sat, 14 Sep 2019 04:24:03 -0600 Subject: [PATCH 077/384] Fix a couple small typos (#100) --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 19c37cf9..96f21e04 100644 --- a/README.md +++ b/README.md @@ -54,7 +54,7 @@ class _ExampleState extends State with SingleTickerProviderStateMixin { } ``` -All widgets that desire to use an `AnimationController` will have to reimplement almost of all this from scratch, which is of course undesired. +All widgets that desire to use an `AnimationController` will have to reimplement almost all of this from scratch, which is of course undesired. Dart mixins can partially solve this issue, but they suffer from other problems: @@ -220,7 +220,7 @@ There are two ways to create a hook: - A function -Functions is by far the most common way to write a hook. Thanks to hooks being composable by nature, a function will be able to combine other hooks to create a custom hook. By convention these functions will be prefixed by `use`. +Functions are by far the most common way to write a hook. Thanks to hooks being composable by nature, a function will be able to combine other hooks to create a custom hook. By convention, these functions will be prefixed by `use`. The following defines a custom hook that creates a variable and logs its value on the console whenever the value changes: From 2b4f40b9f1ad5e7dcc3173adecfb3eecf5e2a446 Mon Sep 17 00:00:00 2001 From: Sahand Akbarzadeh Date: Tue, 8 Oct 2019 13:16:09 +0330 Subject: [PATCH 078/384] implement useReassemble (#103) resolves #60 --- lib/src/misc.dart | 33 +++++++++++++++++++++++++++++++++ test/use_reassemble_test.dart | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 66 insertions(+) create mode 100644 test/use_reassemble_test.dart diff --git a/lib/src/misc.dart b/lib/src/misc.dart index 3e10f1c8..5c34a1ae 100644 --- a/lib/src/misc.dart +++ b/lib/src/misc.dart @@ -110,3 +110,36 @@ class _PreviousHookState extends HookState> { @override T build(BuildContext context) => previous; } + +/// Runs the callback on every hot reload +/// similar to reassemble in the Stateful widgets +/// +/// See also: +/// +/// * [State.reassemble] +void useReassemble(VoidCallback callback) { + assert(() { + Hook.use(_ReassembleHook(callback)); + return true; + }()); +} + +class _ReassembleHook extends Hook { + final VoidCallback callback; + + _ReassembleHook(this.callback) : assert(callback != null); + + @override + _ReassembleHookState createState() => _ReassembleHookState(); +} + +class _ReassembleHookState extends HookState { + @override + void reassemble() { + super.reassemble(); + hook.callback(); + } + + @override + void build(BuildContext context) {} +} diff --git a/test/use_reassemble_test.dart b/test/use_reassemble_test.dart new file mode 100644 index 00000000..d3c59dd5 --- /dev/null +++ b/test/use_reassemble_test.dart @@ -0,0 +1,33 @@ +import 'package:flutter/widgets.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; + +import 'mock.dart'; + +void main() { + testWidgets('useReassemble null callback throws', (tester) async { + await expectPump( + () => tester.pumpWidget(HookBuilder(builder: (c) { + useReassemble(null); + return Container(); + })), + throwsAssertionError, + ); + }); + + testWidgets('hot-reload calls useReassemble\'s callback', (tester) async { + final reassemble = Func0(); + await tester.pumpWidget(HookBuilder(builder: (context) { + useReassemble(reassemble); + return Container(); + })); + + verifyNoMoreInteractions(reassemble); + + hotReload(tester); + await tester.pump(); + + verify(reassemble()).called(1); + verifyNoMoreInteractions(reassemble); + }); + +} From 4bd565fd5e31987ce604bc8a2153baedd7ec90a2 Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Tue, 8 Oct 2019 11:48:41 +0200 Subject: [PATCH 079/384] Changelog + bump version --- CHANGELOG.md | 4 ++++ pubspec.yaml | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8dd94b75..e9e6d115 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.6.1: + +- Added `useReassemble` hook, thanks to @SahandAkbarzadeh + ## 0.6.0: - Make hooks compatible with newer flutter stable version 1.7.8-hotfix.2. diff --git a/pubspec.yaml b/pubspec.yaml index 9715873c..24c9dda2 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -3,7 +3,7 @@ description: A flutter implementation of React hooks. It adds a new kind of widg homepage: https://github.com/rrousselGit/flutter_hooks author: Remi Rousselet -version: 0.6.0 +version: 0.6.1 environment: sdk: ">=2.0.0 <3.0.0" From 1a14d266ba4deefaf7b1fb0c30940b9891565d4d Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Sat, 26 Oct 2019 13:38:39 +0200 Subject: [PATCH 080/384] Add hook to build a text controller --- lib/src/hooks.dart | 6 +- lib/src/text_controller.dart | 59 ++++++++++++++++++ test/use_text_editing_controller_test.dart | 72 ++++++++++++++++++++++ 3 files changed, 135 insertions(+), 2 deletions(-) create mode 100644 lib/src/text_controller.dart create mode 100644 test/use_text_editing_controller_test.dart diff --git a/lib/src/hooks.dart b/lib/src/hooks.dart index c6009c42..005ac468 100644 --- a/lib/src/hooks.dart +++ b/lib/src/hooks.dart @@ -4,8 +4,10 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/scheduler.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_hooks/src/framework.dart'; -part 'async.dart'; + part 'animation.dart'; +part 'async.dart'; +part 'listenable.dart'; part 'misc.dart'; part 'primitives.dart'; -part 'listenable.dart'; +part 'text_controller.dart'; diff --git a/lib/src/text_controller.dart b/lib/src/text_controller.dart new file mode 100644 index 00000000..63fdedd1 --- /dev/null +++ b/lib/src/text_controller.dart @@ -0,0 +1,59 @@ +part of 'hooks.dart'; + +/// Creates an [TextEditingController] that will be disposed automatically. +/// +/// The optional [initialText] parameter can be used to set the initial +/// [TextEditingController.text]. Similarly, [initialValue] can be used to set +/// the initial [TextEditingController.value]. It is invalid to set both +/// [initialText] and [initialValue]. +/// When this hook is re-used with different values of [initialText] or +/// [initialValue], the underlying [TextEditingController] will _not_ be +/// updated. Set values on [TextEditingController.text] or +/// [TextEditingController.value] directly to change the text or selection, +/// respectively. +TextEditingController useTextEditingController( + {String initialText, TextEditingValue initialValue, List keys}) { + return Hook.use(_TextEditingControllerHook(initialText, initialValue, keys)); +} + +class _TextEditingControllerHook extends Hook { + final String initialText; + final TextEditingValue initialValue; + + _TextEditingControllerHook(this.initialText, this.initialValue, + [List keys]) + : assert( + initialText == null || initialValue == null, + "initialText and intialValue can't both be set on a call to " + 'useTextEditingController!', + ), + super(keys: keys); + + @override + HookState createState() { + return _TextEditingControllerHookState(); + } +} + +class _TextEditingControllerHookState + extends HookState { + TextEditingController _controller; + + TextEditingController _constructController() { + if (hook.initialText != null) { + return TextEditingController(text: hook.initialText); + } else if (hook.initialValue != null) { + return TextEditingController.fromValue(hook.initialValue); + } else { + return TextEditingController(); + } + } + + @override + TextEditingController build(BuildContext context) { + return _controller ??= _constructController(); + } + + @override + void dispose() => _controller?.dispose(); +} diff --git a/test/use_text_editing_controller_test.dart b/test/use_text_editing_controller_test.dart new file mode 100644 index 00000000..7b7f1325 --- /dev/null +++ b/test/use_text_editing_controller_test.dart @@ -0,0 +1,72 @@ +import 'package:flutter/widgets.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('throws when both initial text and value is set', (tester) { + return expectPump( + () => tester.pumpWidget(HookBuilder( + builder: (context) { + useTextEditingController( + initialText: 'foo', + initialValue: TextEditingValue.empty, + ); + return Container(); + }, + )), + throwsAssertionError, + ); + }); + + testWidgets('useTextEditingController returns a controller', (tester) async { + TextEditingController controller; + + await tester.pumpWidget(HookBuilder( + builder: (context) { + controller = useTextEditingController(); + return Container(); + }, + )); + + expect(controller, isNotNull); + controller.addListener(() {}); + + // pump another widget so that the old one gets disposed + await tester.pumpWidget(Container()); + + expect(() => controller.addListener(null), throwsA((FlutterError error) { + return error.message.contains('disposed'); + })); + }); + + testWidgets('respects initialText property', (tester) async { + TextEditingController controller; + + await tester.pumpWidget(HookBuilder( + builder: (context) { + controller = useTextEditingController(initialText: 'hello hooks'); + return Container(); + }, + )); + + expect(controller.text, 'hello hooks'); + }); + + testWidgets('respects initialValue property', (tester) async { + const value = TextEditingValue( + text: 'foo', selection: TextSelection.collapsed(offset: 2)); + TextEditingController controller; + + await tester.pumpWidget(HookBuilder( + builder: (context) { + controller = useTextEditingController(initialValue: value); + return Container(); + }, + )); + + expect(controller.value, value); + }); +} From 5f282a12c1fcbcf868f173922841eeb8f9b2859a Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Sat, 26 Oct 2019 18:03:14 +0200 Subject: [PATCH 081/384] Use callable classes to match Flutter api better --- lib/src/text_controller.dart | 47 +++++++++++++++------- test/use_text_editing_controller_test.dart | 23 ++--------- 2 files changed, 37 insertions(+), 33 deletions(-) diff --git a/lib/src/text_controller.dart b/lib/src/text_controller.dart index 63fdedd1..d8f25a50 100644 --- a/lib/src/text_controller.dart +++ b/lib/src/text_controller.dart @@ -1,21 +1,40 @@ part of 'hooks.dart'; -/// Creates an [TextEditingController] that will be disposed automatically. -/// -/// The optional [initialText] parameter can be used to set the initial -/// [TextEditingController.text]. Similarly, [initialValue] can be used to set -/// the initial [TextEditingController.value]. It is invalid to set both -/// [initialText] and [initialValue]. -/// When this hook is re-used with different values of [initialText] or -/// [initialValue], the underlying [TextEditingController] will _not_ be -/// updated. Set values on [TextEditingController.text] or -/// [TextEditingController.value] directly to change the text or selection, -/// respectively. -TextEditingController useTextEditingController( - {String initialText, TextEditingValue initialValue, List keys}) { - return Hook.use(_TextEditingControllerHook(initialText, initialValue, keys)); +class _TextEditingControllerHookCreator { + const _TextEditingControllerHookCreator(); + + /// Creates a [TextEditingController] that will be disposed automatically. + /// + /// The [text] parameter can be used to set the initial value of the + /// controller. + TextEditingController call({String text, List keys}) { + return Hook.use(_TextEditingControllerHook(text, null, keys)); + } + + /// Creates a [TextEditingController] from the initial [value] that will + /// be disposed automatically. + TextEditingController fromValue(TextEditingValue value, [List keys]) { + return Hook.use(_TextEditingControllerHook(null, value, keys)); + } } +/// Functions to create a text editing controller, either via an initial +/// text or an initial [TextEditingValue]. +/// +/// To use a [TextEditingController] with an optional initial text, use +/// [_TextEditingControllerHookCreator.call]: +/// ```dart +/// final controller = useTextEditingController(text: 'initial text'); +/// ``` +/// +/// To use a [TextEditingController] with an optional inital value, use +/// [_TextEditingControllerHookCreator.fromValue]: +/// ```dart +/// final controller = useTextEditingController +/// .fromValue(TextEditingValue.empty); +/// ``` +const useTextEditingController = _TextEditingControllerHookCreator(); + class _TextEditingControllerHook extends Hook { final String initialText; final TextEditingValue initialValue; diff --git a/test/use_text_editing_controller_test.dart b/test/use_text_editing_controller_test.dart index 7b7f1325..fdc57f67 100644 --- a/test/use_text_editing_controller_test.dart +++ b/test/use_text_editing_controller_test.dart @@ -6,21 +6,6 @@ import 'package:flutter_test/flutter_test.dart'; import 'mock.dart'; void main() { - testWidgets('throws when both initial text and value is set', (tester) { - return expectPump( - () => tester.pumpWidget(HookBuilder( - builder: (context) { - useTextEditingController( - initialText: 'foo', - initialValue: TextEditingValue.empty, - ); - return Container(); - }, - )), - throwsAssertionError, - ); - }); - testWidgets('useTextEditingController returns a controller', (tester) async { TextEditingController controller; @@ -42,12 +27,12 @@ void main() { })); }); - testWidgets('respects initialText property', (tester) async { + testWidgets('respects initial text property', (tester) async { TextEditingController controller; await tester.pumpWidget(HookBuilder( builder: (context) { - controller = useTextEditingController(initialText: 'hello hooks'); + controller = useTextEditingController(text: 'hello hooks'); return Container(); }, )); @@ -55,14 +40,14 @@ void main() { expect(controller.text, 'hello hooks'); }); - testWidgets('respects initialValue property', (tester) async { + testWidgets('respects initial value property', (tester) async { const value = TextEditingValue( text: 'foo', selection: TextSelection.collapsed(offset: 2)); TextEditingController controller; await tester.pumpWidget(HookBuilder( builder: (context) { - controller = useTextEditingController(initialValue: value); + controller = useTextEditingController.fromValue(value); return Container(); }, )); From 8bf67d5f43dfa0c3bc17da7661c0baa9b7ec5b71 Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Mon, 28 Oct 2019 16:05:10 +0100 Subject: [PATCH 082/384] Review feedback --- lib/src/text_controller.dart | 43 +++++++++++++++------- test/use_text_editing_controller_test.dart | 43 +++++++++++++++++++--- 2 files changed, 66 insertions(+), 20 deletions(-) diff --git a/lib/src/text_controller.dart b/lib/src/text_controller.dart index d8f25a50..d9eb661a 100644 --- a/lib/src/text_controller.dart +++ b/lib/src/text_controller.dart @@ -18,21 +18,39 @@ class _TextEditingControllerHookCreator { } } -/// Functions to create a text editing controller, either via an initial -/// text or an initial [TextEditingValue]. +/// Creates a [TextEditingController], either via an initial text or an initial +/// [TextEditingValue]. /// /// To use a [TextEditingController] with an optional initial text, use -/// [_TextEditingControllerHookCreator.call]: /// ```dart /// final controller = useTextEditingController(text: 'initial text'); /// ``` /// /// To use a [TextEditingController] with an optional inital value, use -/// [_TextEditingControllerHookCreator.fromValue]: /// ```dart /// final controller = useTextEditingController /// .fromValue(TextEditingValue.empty); /// ``` +/// +/// Changing the text or initial value after the widget has been built has no +/// effect whatsoever. To update the value in a callback, for instance after a +/// button was pressed, use the [TextEditingController.text] or +/// [TextEditingController.value] setters. To have the [TextEditingController] +/// reflect changing values, you can use [useEffect]. This example will update +/// the [TextEditingController.text] whenever a provided [ValueListenable] +/// changes: +/// ```dart +/// final controller = useTextEditingController(); +/// final update = useValueListenable(myTextControllerUpdates); +/// +/// useEffect(() { +/// controller.text = update; +/// return null; // we don't need to have a special dispose logic +/// }, [update]); +/// ``` +/// +/// See also: +/// - [TextEditingController], which this hook creates. const useTextEditingController = _TextEditingControllerHookCreator(); class _TextEditingControllerHook extends Hook { @@ -49,7 +67,7 @@ class _TextEditingControllerHook extends Hook { super(keys: keys); @override - HookState createState() { + _TextEditingControllerHookState createState() { return _TextEditingControllerHookState(); } } @@ -58,20 +76,17 @@ class _TextEditingControllerHookState extends HookState { TextEditingController _controller; - TextEditingController _constructController() { - if (hook.initialText != null) { - return TextEditingController(text: hook.initialText); - } else if (hook.initialValue != null) { - return TextEditingController.fromValue(hook.initialValue); + @override + void initHook() { + if (hook.initialValue != null) { + _controller = TextEditingController.fromValue(hook.initialValue); } else { - return TextEditingController(); + _controller = TextEditingController(text: hook.initialText); } } @override - TextEditingController build(BuildContext context) { - return _controller ??= _constructController(); - } + TextEditingController build(BuildContext context) => _controller; @override void dispose() => _controller?.dispose(); diff --git a/test/use_text_editing_controller_test.dart b/test/use_text_editing_controller_test.dart index fdc57f67..24a484b9 100644 --- a/test/use_text_editing_controller_test.dart +++ b/test/use_text_editing_controller_test.dart @@ -1,3 +1,4 @@ +import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_hooks/src/framework.dart'; import 'package:flutter_hooks/src/hooks.dart'; @@ -7,11 +8,13 @@ import 'mock.dart'; void main() { testWidgets('useTextEditingController returns a controller', (tester) async { + final rebuilder = ValueNotifier(0); TextEditingController controller; await tester.pumpWidget(HookBuilder( builder: (context) { controller = useTextEditingController(); + useValueListenable(rebuilder); return Container(); }, )); @@ -19,6 +22,13 @@ void main() { expect(controller, isNotNull); controller.addListener(() {}); + // rebuild hook + final firstController = controller; + rebuilder.notifyListeners(); + await tester.pumpAndSettle(); + expect(identical(controller, firstController), isTrue, + reason: 'Controllers should be identical after rebuilds'); + // pump another widget so that the old one gets disposed await tester.pumpWidget(Container()); @@ -28,30 +38,51 @@ void main() { }); testWidgets('respects initial text property', (tester) async { + final rebuilder = ValueNotifier(0); TextEditingController controller; + const initialText = 'hello hooks'; + var targetText = initialText; await tester.pumpWidget(HookBuilder( builder: (context) { - controller = useTextEditingController(text: 'hello hooks'); + controller = useTextEditingController(text: targetText); + useValueListenable(rebuilder); return Container(); }, )); - expect(controller.text, 'hello hooks'); + expect(controller.text, targetText); + + // change text and rebuild - the value of the controler shouldn't change + targetText = "can't see me!"; + rebuilder.notifyListeners(); + await tester.pumpAndSettle(); + expect(controller.text, initialText); }); testWidgets('respects initial value property', (tester) async { - const value = TextEditingValue( - text: 'foo', selection: TextSelection.collapsed(offset: 2)); + final rebuilder = ValueNotifier(0); + const initialValue = TextEditingValue( + text: 'foo', + selection: TextSelection.collapsed(offset: 2), + ); + var targetValue = initialValue; TextEditingController controller; await tester.pumpWidget(HookBuilder( builder: (context) { - controller = useTextEditingController.fromValue(value); + controller = useTextEditingController.fromValue(targetValue); + useValueListenable(rebuilder); return Container(); }, )); - expect(controller.value, value); + expect(controller.value, targetValue); + + // similar to above - the value should not change after a rebuild + targetValue = const TextEditingValue(text: 'another'); + rebuilder.notifyListeners(); + await tester.pumpAndSettle(); + expect(controller.value, initialValue); }); } From ab8e87e8615ce7d7622a85efa89989ada1d9181a Mon Sep 17 00:00:00 2001 From: smiLLe Date: Thu, 31 Oct 2019 17:44:10 +0100 Subject: [PATCH 083/384] added basic star wars example based on provider and built_value --- example/lib/main.dart | 5 + example/lib/star_wars/app_state.dart | 11 + example/lib/star_wars/app_state.g.dart | 101 ++++++ example/lib/star_wars/hooks.dart | 36 ++ example/lib/star_wars/models.dart | 46 +++ example/lib/star_wars/models.g.dart | 431 +++++++++++++++++++++++ example/lib/star_wars/planet_list.dart | 151 ++++++++ example/lib/star_wars/redux.dart | 22 ++ example/lib/star_wars/star_wars_api.dart | 18 + example/pubspec.lock | 364 ++++++++++++++++++- example/pubspec.yaml | 8 +- example/test/widget_test.dart | 30 ++ 12 files changed, 1208 insertions(+), 15 deletions(-) create mode 100644 example/lib/star_wars/app_state.dart create mode 100644 example/lib/star_wars/app_state.g.dart create mode 100644 example/lib/star_wars/hooks.dart create mode 100644 example/lib/star_wars/models.dart create mode 100644 example/lib/star_wars/models.g.dart create mode 100644 example/lib/star_wars/planet_list.dart create mode 100644 example/lib/star_wars/redux.dart create mode 100644 example/lib/star_wars/star_wars_api.dart create mode 100644 example/test/widget_test.dart diff --git a/example/lib/main.dart b/example/lib/main.dart index 5a28c85d..d3baa12e 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -1,5 +1,6 @@ // ignore_for_file: omit_local_variable_types import 'package:flutter/material.dart'; +import 'package:flutter_hooks_gallery/star_wars/planet_list.dart'; import 'package:flutter_hooks_gallery/use_effect.dart'; import 'package:flutter_hooks_gallery/use_state.dart'; import 'package:flutter_hooks_gallery/use_stream.dart'; @@ -31,6 +32,10 @@ class HooksGalleryApp extends StatelessWidget { title: 'Custom Hook Function', builder: (context) => CustomHookExample(), ), + _GalleryItem( + title: 'Star Wars Planets', + builder: (context) => PlanetList(), + ) ]), ), ); diff --git a/example/lib/star_wars/app_state.dart b/example/lib/star_wars/app_state.dart new file mode 100644 index 00000000..67805852 --- /dev/null +++ b/example/lib/star_wars/app_state.dart @@ -0,0 +1,11 @@ +import 'package:built_value/built_value.dart'; +import 'package:flutter_hooks_gallery/star_wars/models.dart'; + +part 'app_state.g.dart'; + +abstract class AppState implements Built { + PlanetPageModel get planetPage; + + AppState._(); + factory AppState([void Function(AppStateBuilder) updates]) = _$AppState; +} diff --git a/example/lib/star_wars/app_state.g.dart b/example/lib/star_wars/app_state.g.dart new file mode 100644 index 00000000..0e4c9e52 --- /dev/null +++ b/example/lib/star_wars/app_state.g.dart @@ -0,0 +1,101 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'app_state.dart'; + +// ************************************************************************** +// BuiltValueGenerator +// ************************************************************************** + +class _$AppState extends AppState { + @override + final PlanetPageModel planetPage; + + factory _$AppState([void Function(AppStateBuilder) updates]) => + (new AppStateBuilder()..update(updates)).build(); + + _$AppState._({this.planetPage}) : super._() { + if (planetPage == null) { + throw new BuiltValueNullFieldError('AppState', 'planetPage'); + } + } + + @override + AppState rebuild(void Function(AppStateBuilder) updates) => + (toBuilder()..update(updates)).build(); + + @override + AppStateBuilder toBuilder() => new AppStateBuilder()..replace(this); + + @override + bool operator ==(Object other) { + if (identical(other, this)) return true; + return other is AppState && planetPage == other.planetPage; + } + + @override + int get hashCode { + return $jf($jc(0, planetPage.hashCode)); + } + + @override + String toString() { + return (newBuiltValueToStringHelper('AppState') + ..add('planetPage', planetPage)) + .toString(); + } +} + +class AppStateBuilder implements Builder { + _$AppState _$v; + + PlanetPageModelBuilder _planetPage; + PlanetPageModelBuilder get planetPage => + _$this._planetPage ??= new PlanetPageModelBuilder(); + set planetPage(PlanetPageModelBuilder planetPage) => + _$this._planetPage = planetPage; + + AppStateBuilder(); + + AppStateBuilder get _$this { + if (_$v != null) { + _planetPage = _$v.planetPage?.toBuilder(); + _$v = null; + } + return this; + } + + @override + void replace(AppState other) { + if (other == null) { + throw new ArgumentError.notNull('other'); + } + _$v = other as _$AppState; + } + + @override + void update(void Function(AppStateBuilder) updates) { + if (updates != null) updates(this); + } + + @override + _$AppState build() { + _$AppState _$result; + try { + _$result = _$v ?? new _$AppState._(planetPage: planetPage.build()); + } catch (_) { + String _$failedField; + try { + _$failedField = 'planetPage'; + planetPage.build(); + } catch (e) { + throw new BuiltValueNestedFieldError( + 'AppState', _$failedField, e.toString()); + } + rethrow; + } + replace(_$result); + return _$result; + } +} + +// ignore_for_file: always_put_control_body_on_new_line,always_specify_types,annotate_overrides,avoid_annotating_with_dynamic,avoid_as,avoid_catches_without_on_clauses,avoid_returning_this,lines_longer_than_80_chars,omit_local_variable_types,prefer_expression_function_bodies,sort_constructors_first,test_types_in_equals,unnecessary_const,unnecessary_new diff --git a/example/lib/star_wars/hooks.dart b/example/lib/star_wars/hooks.dart new file mode 100644 index 00000000..63b75f1e --- /dev/null +++ b/example/lib/star_wars/hooks.dart @@ -0,0 +1,36 @@ +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:flutter_hooks_gallery/star_wars/app_state.dart'; +import 'package:flutter_hooks_gallery/star_wars/redux.dart'; +import 'package:flutter_hooks_gallery/star_wars/star_wars_api.dart'; +import 'package:provider/provider.dart'; + +typedef FetchAndDispatchPlanets = Future Function(String); + +/// return the redux store created by [useReducer} +/// We use [Provider] to retrieve the redux store. +Store useAppStore() { + final context = useContext(); + return Provider.of>(context); +} + +/// return [AppState] hold by redux store. +/// We use [Provider] to retrieve the [AppState]. +/// This will also rebuild whenever the [AppState] has been changed +AppState useAppState() { + final context = useContext(); + return Provider.of(context); +} + +/// return star wars api. +/// We use [Provider] to retrieve the [StarWarsApi]. +StarWarsApi useStarWarsApi() { + final context = useContext(); + return Provider.of(context); +} + +/// "middleware" to load data and update state +/// We use [Provider] to retrieve the [FetchAndDispatchPlanets] "middleware". +FetchAndDispatchPlanets useFetchAndDispatchPlanets() { + final context = useContext(); + return Provider.of(context); +} diff --git a/example/lib/star_wars/models.dart b/example/lib/star_wars/models.dart new file mode 100644 index 00000000..aa15bb88 --- /dev/null +++ b/example/lib/star_wars/models.dart @@ -0,0 +1,46 @@ +// ignore_for_file: public_member_api_docs + +import 'package:built_collection/built_collection.dart'; +import 'package:built_value/built_value.dart'; +import 'package:built_value/serializer.dart'; +import 'package:built_value/standard_json_plugin.dart'; + +part 'models.g.dart'; + +@SerializersFor([ + PlanetPageModel, + PlanetModel, +]) +final Serializers serializers = + (_$serializers.toBuilder()..addPlugin(StandardJsonPlugin())).build(); + +abstract class PlanetPageModel + implements Built { + static Serializer get serializer => + _$planetPageModelSerializer; + @nullable + String get next; + + @nullable + String get previous; + + BuiltList get results; + + PlanetPageModel._(); + factory PlanetPageModel([void Function(PlanetPageModelBuilder) updates]) = + _$PlanetPageModel; +} + +abstract class PlanetModel implements Built { + static Serializer get serializer => _$planetModelSerializer; + String get name; + String get diameter; + String get climate; + String get terrain; + String get population; + String get url; + + PlanetModel._(); + factory PlanetModel([void Function(PlanetModelBuilder) updates]) = + _$PlanetModel; +} diff --git a/example/lib/star_wars/models.g.dart b/example/lib/star_wars/models.g.dart new file mode 100644 index 00000000..9e16c720 --- /dev/null +++ b/example/lib/star_wars/models.g.dart @@ -0,0 +1,431 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'models.dart'; + +// ************************************************************************** +// BuiltValueGenerator +// ************************************************************************** + +Serializers _$serializers = (new Serializers().toBuilder() + ..add(PlanetModel.serializer) + ..add(PlanetPageModel.serializer) + ..addBuilderFactory( + const FullType(BuiltList, const [const FullType(PlanetModel)]), + () => new ListBuilder())) + .build(); +Serializer _$planetPageModelSerializer = + new _$PlanetPageModelSerializer(); +Serializer _$planetModelSerializer = new _$PlanetModelSerializer(); + +class _$PlanetPageModelSerializer + implements StructuredSerializer { + @override + final Iterable types = const [PlanetPageModel, _$PlanetPageModel]; + @override + final String wireName = 'PlanetPageModel'; + + @override + Iterable serialize(Serializers serializers, PlanetPageModel object, + {FullType specifiedType = FullType.unspecified}) { + final result = [ + 'results', + serializers.serialize(object.results, + specifiedType: + const FullType(BuiltList, const [const FullType(PlanetModel)])), + ]; + if (object.next != null) { + result + ..add('next') + ..add(serializers.serialize(object.next, + specifiedType: const FullType(String))); + } + if (object.previous != null) { + result + ..add('previous') + ..add(serializers.serialize(object.previous, + specifiedType: const FullType(String))); + } + return result; + } + + @override + PlanetPageModel deserialize( + Serializers serializers, Iterable serialized, + {FullType specifiedType = FullType.unspecified}) { + final result = new PlanetPageModelBuilder(); + + final iterator = serialized.iterator; + while (iterator.moveNext()) { + final key = iterator.current as String; + iterator.moveNext(); + final dynamic value = iterator.current; + switch (key) { + case 'next': + result.next = serializers.deserialize(value, + specifiedType: const FullType(String)) as String; + break; + case 'previous': + result.previous = serializers.deserialize(value, + specifiedType: const FullType(String)) as String; + break; + case 'results': + result.results.replace(serializers.deserialize(value, + specifiedType: const FullType( + BuiltList, const [const FullType(PlanetModel)])) + as BuiltList); + break; + } + } + + return result.build(); + } +} + +class _$PlanetModelSerializer implements StructuredSerializer { + @override + final Iterable types = const [PlanetModel, _$PlanetModel]; + @override + final String wireName = 'PlanetModel'; + + @override + Iterable serialize(Serializers serializers, PlanetModel object, + {FullType specifiedType = FullType.unspecified}) { + final result = [ + 'name', + serializers.serialize(object.name, specifiedType: const FullType(String)), + 'diameter', + serializers.serialize(object.diameter, + specifiedType: const FullType(String)), + 'climate', + serializers.serialize(object.climate, + specifiedType: const FullType(String)), + 'terrain', + serializers.serialize(object.terrain, + specifiedType: const FullType(String)), + 'population', + serializers.serialize(object.population, + specifiedType: const FullType(String)), + 'url', + serializers.serialize(object.url, specifiedType: const FullType(String)), + ]; + + return result; + } + + @override + PlanetModel deserialize(Serializers serializers, Iterable serialized, + {FullType specifiedType = FullType.unspecified}) { + final result = new PlanetModelBuilder(); + + final iterator = serialized.iterator; + while (iterator.moveNext()) { + final key = iterator.current as String; + iterator.moveNext(); + final dynamic value = iterator.current; + switch (key) { + case 'name': + result.name = serializers.deserialize(value, + specifiedType: const FullType(String)) as String; + break; + case 'diameter': + result.diameter = serializers.deserialize(value, + specifiedType: const FullType(String)) as String; + break; + case 'climate': + result.climate = serializers.deserialize(value, + specifiedType: const FullType(String)) as String; + break; + case 'terrain': + result.terrain = serializers.deserialize(value, + specifiedType: const FullType(String)) as String; + break; + case 'population': + result.population = serializers.deserialize(value, + specifiedType: const FullType(String)) as String; + break; + case 'url': + result.url = serializers.deserialize(value, + specifiedType: const FullType(String)) as String; + break; + } + } + + return result.build(); + } +} + +class _$PlanetPageModel extends PlanetPageModel { + @override + final String next; + @override + final String previous; + @override + final BuiltList results; + + factory _$PlanetPageModel([void Function(PlanetPageModelBuilder) updates]) => + (new PlanetPageModelBuilder()..update(updates)).build(); + + _$PlanetPageModel._({this.next, this.previous, this.results}) : super._() { + if (results == null) { + throw new BuiltValueNullFieldError('PlanetPageModel', 'results'); + } + } + + @override + PlanetPageModel rebuild(void Function(PlanetPageModelBuilder) updates) => + (toBuilder()..update(updates)).build(); + + @override + PlanetPageModelBuilder toBuilder() => + new PlanetPageModelBuilder()..replace(this); + + @override + bool operator ==(Object other) { + if (identical(other, this)) return true; + return other is PlanetPageModel && + next == other.next && + previous == other.previous && + results == other.results; + } + + @override + int get hashCode { + return $jf( + $jc($jc($jc(0, next.hashCode), previous.hashCode), results.hashCode)); + } + + @override + String toString() { + return (newBuiltValueToStringHelper('PlanetPageModel') + ..add('next', next) + ..add('previous', previous) + ..add('results', results)) + .toString(); + } +} + +class PlanetPageModelBuilder + implements Builder { + _$PlanetPageModel _$v; + + String _next; + String get next => _$this._next; + set next(String next) => _$this._next = next; + + String _previous; + String get previous => _$this._previous; + set previous(String previous) => _$this._previous = previous; + + ListBuilder _results; + ListBuilder get results => + _$this._results ??= new ListBuilder(); + set results(ListBuilder results) => _$this._results = results; + + PlanetPageModelBuilder(); + + PlanetPageModelBuilder get _$this { + if (_$v != null) { + _next = _$v.next; + _previous = _$v.previous; + _results = _$v.results?.toBuilder(); + _$v = null; + } + return this; + } + + @override + void replace(PlanetPageModel other) { + if (other == null) { + throw new ArgumentError.notNull('other'); + } + _$v = other as _$PlanetPageModel; + } + + @override + void update(void Function(PlanetPageModelBuilder) updates) { + if (updates != null) updates(this); + } + + @override + _$PlanetPageModel build() { + _$PlanetPageModel _$result; + try { + _$result = _$v ?? + new _$PlanetPageModel._( + next: next, previous: previous, results: results.build()); + } catch (_) { + String _$failedField; + try { + _$failedField = 'results'; + results.build(); + } catch (e) { + throw new BuiltValueNestedFieldError( + 'PlanetPageModel', _$failedField, e.toString()); + } + rethrow; + } + replace(_$result); + return _$result; + } +} + +class _$PlanetModel extends PlanetModel { + @override + final String name; + @override + final String diameter; + @override + final String climate; + @override + final String terrain; + @override + final String population; + @override + final String url; + + factory _$PlanetModel([void Function(PlanetModelBuilder) updates]) => + (new PlanetModelBuilder()..update(updates)).build(); + + _$PlanetModel._( + {this.name, + this.diameter, + this.climate, + this.terrain, + this.population, + this.url}) + : super._() { + if (name == null) { + throw new BuiltValueNullFieldError('PlanetModel', 'name'); + } + if (diameter == null) { + throw new BuiltValueNullFieldError('PlanetModel', 'diameter'); + } + if (climate == null) { + throw new BuiltValueNullFieldError('PlanetModel', 'climate'); + } + if (terrain == null) { + throw new BuiltValueNullFieldError('PlanetModel', 'terrain'); + } + if (population == null) { + throw new BuiltValueNullFieldError('PlanetModel', 'population'); + } + if (url == null) { + throw new BuiltValueNullFieldError('PlanetModel', 'url'); + } + } + + @override + PlanetModel rebuild(void Function(PlanetModelBuilder) updates) => + (toBuilder()..update(updates)).build(); + + @override + PlanetModelBuilder toBuilder() => new PlanetModelBuilder()..replace(this); + + @override + bool operator ==(Object other) { + if (identical(other, this)) return true; + return other is PlanetModel && + name == other.name && + diameter == other.diameter && + climate == other.climate && + terrain == other.terrain && + population == other.population && + url == other.url; + } + + @override + int get hashCode { + return $jf($jc( + $jc( + $jc( + $jc($jc($jc(0, name.hashCode), diameter.hashCode), + climate.hashCode), + terrain.hashCode), + population.hashCode), + url.hashCode)); + } + + @override + String toString() { + return (newBuiltValueToStringHelper('PlanetModel') + ..add('name', name) + ..add('diameter', diameter) + ..add('climate', climate) + ..add('terrain', terrain) + ..add('population', population) + ..add('url', url)) + .toString(); + } +} + +class PlanetModelBuilder implements Builder { + _$PlanetModel _$v; + + String _name; + String get name => _$this._name; + set name(String name) => _$this._name = name; + + String _diameter; + String get diameter => _$this._diameter; + set diameter(String diameter) => _$this._diameter = diameter; + + String _climate; + String get climate => _$this._climate; + set climate(String climate) => _$this._climate = climate; + + String _terrain; + String get terrain => _$this._terrain; + set terrain(String terrain) => _$this._terrain = terrain; + + String _population; + String get population => _$this._population; + set population(String population) => _$this._population = population; + + String _url; + String get url => _$this._url; + set url(String url) => _$this._url = url; + + PlanetModelBuilder(); + + PlanetModelBuilder get _$this { + if (_$v != null) { + _name = _$v.name; + _diameter = _$v.diameter; + _climate = _$v.climate; + _terrain = _$v.terrain; + _population = _$v.population; + _url = _$v.url; + _$v = null; + } + return this; + } + + @override + void replace(PlanetModel other) { + if (other == null) { + throw new ArgumentError.notNull('other'); + } + _$v = other as _$PlanetModel; + } + + @override + void update(void Function(PlanetModelBuilder) updates) { + if (updates != null) updates(this); + } + + @override + _$PlanetModel build() { + final _$result = _$v ?? + new _$PlanetModel._( + name: name, + diameter: diameter, + climate: climate, + terrain: terrain, + population: population, + url: url); + replace(_$result); + return _$result; + } +} + +// ignore_for_file: always_put_control_body_on_new_line,always_specify_types,annotate_overrides,avoid_annotating_with_dynamic,avoid_as,avoid_catches_without_on_clauses,avoid_returning_this,lines_longer_than_80_chars,omit_local_variable_types,prefer_expression_function_bodies,sort_constructors_first,test_types_in_equals,unnecessary_const,unnecessary_new diff --git a/example/lib/star_wars/planet_list.dart b/example/lib/star_wars/planet_list.dart new file mode 100644 index 00000000..30946b06 --- /dev/null +++ b/example/lib/star_wars/planet_list.dart @@ -0,0 +1,151 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:flutter_hooks_gallery/star_wars/app_state.dart'; +import 'package:flutter_hooks_gallery/star_wars/hooks.dart'; +import 'package:flutter_hooks_gallery/star_wars/redux.dart'; +import 'package:flutter_hooks_gallery/star_wars/star_wars_api.dart'; +import 'package:provider/provider.dart'; + +/// This example will load, show and let you navigate through all star wars +/// planets. +/// +/// It will demonstrate on how to use [Provider] and redux ([useReducer]) +class PlanetList extends HookWidget { + @override + Widget build(BuildContext context) { + /// create single instance of Star Wars Api + final api = useMemoized(() => StarWarsApi()); + + /// create single instance of redux store + final store = useReducer( + reducer, + initialState: AppState(), + ); + + return Scaffold( + appBar: AppBar( + title: const Text( + 'Star Wars Planets', + ), + ), + + /// provide star wars api instance, + /// redux store instance + /// and redux state down the widget tree. + body: MultiProvider( + providers: [ + Provider.value(value: api), + Provider.value(value: store), + Provider.value(value: store.state), + ], + child: HookBuilder( + builder: (context) { + final state = useAppState(); + final store = useAppStore(); + final isLoadingState = useState(false); + + final fetchAndDispatchPlanets = + useMemoized( + () => ([String url]) async { + isLoadingState.value = true; + final page = await api.getPlanets(url); + store.dispatch(SetPlanetPageAction(page)); + isLoadingState.value = false; + }, + [store], + ); + + final buttonAlignment = useMemoized( + () { + if (null == state.planetPage.previous) { + return MainAxisAlignment.end; + } + if (null == state.planetPage.next) { + return MainAxisAlignment.start; + } + return MainAxisAlignment.spaceBetween; + }, + [state], + ); + + /// load the first planet page but only on the first build + useEffect( + () { + fetchAndDispatchPlanets(null); + return () {}; + }, + const [], + ); + + return Provider.value( + value: fetchAndDispatchPlanets, + child: CustomScrollView( + slivers: [ + if (isLoadingState.value) + SliverToBoxAdapter( + child: Center(child: const CircularProgressIndicator()), + ), + if (!isLoadingState.value) + SliverToBoxAdapter( + child: Row( + mainAxisAlignment: buttonAlignment, + children: [ + if (null != state.planetPage.previous) + _LoadPageButton( + next: false, + ), + if (null != state.planetPage.next) + _LoadPageButton( + next: true, + ) + ], + ), + ), + if (!isLoadingState.value && + state.planetPage.results.isNotEmpty) + _List(), + ], + ), + ); + }, + ), + ), + ); + } +} + +class _LoadPageButton extends HookWidget { + final bool next; + + _LoadPageButton({this.next = true}) : assert(next != null); + + @override + Widget build(BuildContext context) { + final state = useAppState(); + final fetchAndDispatch = useFetchAndDispatchPlanets(); + + return RaisedButton( + child: next ? const Text('Next Page') : const Text('Prev Page'), + onPressed: () async { + final url = next ? state.planetPage.next : state.planetPage.previous; + await fetchAndDispatch(url); + }, + ); + } +} + +class _List extends HookWidget { + @override + Widget build(BuildContext context) { + final state = useAppState(); + return SliverList( + delegate: SliverChildListDelegate( + state.planetPage.results + .map((planet) => ListTile( + title: Text(planet.name), + )) + .toList(), + ), + ); + } +} diff --git a/example/lib/star_wars/redux.dart b/example/lib/star_wars/redux.dart new file mode 100644 index 00000000..15865fd0 --- /dev/null +++ b/example/lib/star_wars/redux.dart @@ -0,0 +1,22 @@ +// ignore_for_file: public_member_api_docs + +import 'package:flutter_hooks_gallery/star_wars/app_state.dart'; +import 'package:flutter_hooks_gallery/star_wars/models.dart'; + +abstract class ReduxAction {} + +class SetPlanetPageAction extends ReduxAction { + final PlanetPageModel page; + + SetPlanetPageAction(this.page); +} + +/// reducer that is used by [useReducer] to create the redux store +AppState reducer(S state, A action) { + final b = state.toBuilder(); + if (action is SetPlanetPageAction) { + b.planetPage.replace(action.page); + } + + return b.build(); +} diff --git a/example/lib/star_wars/star_wars_api.dart b/example/lib/star_wars/star_wars_api.dart new file mode 100644 index 00000000..dc7cecf0 --- /dev/null +++ b/example/lib/star_wars/star_wars_api.dart @@ -0,0 +1,18 @@ +// ignore_for_file: public_member_api_docs + +import 'dart:convert'; + +import 'package:flutter_hooks_gallery/star_wars/models.dart'; +import 'package:http/http.dart' as http; + +/// Api wrapper to retrieve Star Wars related data +class StarWarsApi { + /// load and return one page of planets + Future getPlanets(String page) async { + page ??= 'https://swapi.co/api/planets'; + final response = await http.get(page); + dynamic json = jsonDecode(utf8.decode(response.bodyBytes)); + + return serializers.deserializeWith(PlanetPageModel.serializer, json); + } +} diff --git a/example/pubspec.lock b/example/pubspec.lock index 01000bce..aef9bde3 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -1,20 +1,104 @@ # Generated by pub -# See https://www.dartlang.org/tools/pub/glossary#lockfile +# See https://dart.dev/tools/pub/glossary#lockfile packages: + analyzer: + dependency: transitive + description: + name: analyzer + url: "https://pub.dartlang.org" + source: hosted + version: "0.38.5" + analyzer_plugin: + dependency: transitive + description: + name: analyzer_plugin + url: "https://pub.dartlang.org" + source: hosted + version: "0.2.1" + args: + dependency: transitive + description: + name: args + url: "https://pub.dartlang.org" + source: hosted + version: "1.5.2" async: dependency: transitive description: name: async url: "https://pub.dartlang.org" source: hosted - version: "2.0.8" + version: "2.3.0" boolean_selector: dependency: transitive description: name: boolean_selector url: "https://pub.dartlang.org" source: hosted - version: "1.0.4" + version: "1.0.5" + build: + dependency: transitive + description: + name: build + url: "https://pub.dartlang.org" + source: hosted + version: "1.2.1" + build_config: + dependency: transitive + description: + name: build_config + url: "https://pub.dartlang.org" + source: hosted + version: "0.4.1+1" + build_daemon: + dependency: transitive + description: + name: build_daemon + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.0" + build_resolvers: + dependency: transitive + description: + name: build_resolvers + url: "https://pub.dartlang.org" + source: hosted + version: "1.2.1" + build_runner: + dependency: "direct dev" + description: + name: build_runner + url: "https://pub.dartlang.org" + source: hosted + version: "1.7.1" + build_runner_core: + dependency: transitive + description: + name: build_runner_core + url: "https://pub.dartlang.org" + source: hosted + version: "4.1.0" + built_collection: + dependency: "direct main" + description: + name: built_collection + url: "https://pub.dartlang.org" + source: hosted + version: "4.2.2" + built_value: + dependency: "direct main" + description: + name: built_value + url: "https://pub.dartlang.org" + source: hosted + version: "6.8.2" + built_value_generator: + dependency: "direct dev" + description: + name: built_value_generator + url: "https://pub.dartlang.org" + source: hosted + version: "6.8.2" charcode: dependency: transitive description: @@ -22,6 +106,20 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.1.2" + checked_yaml: + dependency: transitive + description: + name: checked_yaml + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.2" + code_builder: + dependency: transitive + description: + name: code_builder + url: "https://pub.dartlang.org" + source: hosted + version: "3.2.0" collection: dependency: transitive description: @@ -29,6 +127,41 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.14.11" + convert: + dependency: transitive + description: + name: convert + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.1" + crypto: + dependency: transitive + description: + name: crypto + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.3" + csslib: + dependency: transitive + description: + name: csslib + url: "https://pub.dartlang.org" + source: hosted + version: "0.16.1" + dart_style: + dependency: transitive + description: + name: dart_style + url: "https://pub.dartlang.org" + source: hosted + version: "1.3.3" + fixnum: + dependency: transitive + description: + name: fixnum + url: "https://pub.dartlang.org" + source: hosted + version: "0.10.11" flutter: dependency: "direct main" description: flutter @@ -40,47 +173,194 @@ packages: path: ".." relative: true source: path - version: "0.4.0" + version: "0.6.1" flutter_test: dependency: "direct dev" description: flutter source: sdk version: "0.0.0" + front_end: + dependency: transitive + description: + name: front_end + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.27" + glob: + dependency: transitive + description: + name: glob + url: "https://pub.dartlang.org" + source: hosted + version: "1.2.0" + graphs: + dependency: transitive + description: + name: graphs + url: "https://pub.dartlang.org" + source: hosted + version: "0.2.0" + html: + dependency: transitive + description: + name: html + url: "https://pub.dartlang.org" + source: hosted + version: "0.14.0+3" + http: + dependency: transitive + description: + name: http + url: "https://pub.dartlang.org" + source: hosted + version: "0.12.0+2" + http_multi_server: + dependency: transitive + description: + name: http_multi_server + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.0" + http_parser: + dependency: transitive + description: + name: http_parser + url: "https://pub.dartlang.org" + source: hosted + version: "3.1.3" + io: + dependency: transitive + description: + name: io + url: "https://pub.dartlang.org" + source: hosted + version: "0.3.3" + js: + dependency: transitive + description: + name: js + url: "https://pub.dartlang.org" + source: hosted + version: "0.6.1+1" + json_annotation: + dependency: transitive + description: + name: json_annotation + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.0" + kernel: + dependency: transitive + description: + name: kernel + url: "https://pub.dartlang.org" + source: hosted + version: "0.3.27" + logging: + dependency: transitive + description: + name: logging + url: "https://pub.dartlang.org" + source: hosted + version: "0.11.3+2" matcher: dependency: transitive description: name: matcher url: "https://pub.dartlang.org" source: hosted - version: "0.12.3+1" + version: "0.12.5" meta: dependency: transitive description: name: meta url: "https://pub.dartlang.org" source: hosted - version: "1.1.6" + version: "1.1.7" + mime: + dependency: transitive + description: + name: mime + url: "https://pub.dartlang.org" + source: hosted + version: "0.9.6+3" + node_interop: + dependency: transitive + description: + name: node_interop + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.3" + node_io: + dependency: transitive + description: + name: node_io + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.1+2" + package_config: + dependency: transitive + description: + name: package_config + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.0" + package_resolver: + dependency: transitive + description: + name: package_resolver + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.10" path: dependency: transitive description: name: path url: "https://pub.dartlang.org" source: hosted - version: "1.6.2" + version: "1.6.4" pedantic: dependency: transitive description: name: pedantic url: "https://pub.dartlang.org" source: hosted + version: "1.8.0+1" + pool: + dependency: transitive + description: + name: pool + url: "https://pub.dartlang.org" + source: hosted version: "1.4.0" + provider: + dependency: "direct main" + description: + name: provider + url: "https://pub.dartlang.org" + source: hosted + version: "3.1.0+1" + pub_semver: + dependency: transitive + description: + name: pub_semver + url: "https://pub.dartlang.org" + source: hosted + version: "1.4.2" + pubspec_parse: + dependency: transitive + description: + name: pubspec_parse + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.5" quiver: dependency: transitive description: name: quiver url: "https://pub.dartlang.org" source: hosted - version: "2.0.1" + version: "2.0.5" shared_preferences: dependency: "direct main" description: @@ -88,18 +368,39 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.4.3" + shelf: + dependency: transitive + description: + name: shelf + url: "https://pub.dartlang.org" + source: hosted + version: "0.7.5" + shelf_web_socket: + dependency: transitive + description: + name: shelf_web_socket + url: "https://pub.dartlang.org" + source: hosted + version: "0.2.3" sky_engine: dependency: transitive description: flutter source: sdk version: "0.0.99" + source_gen: + dependency: transitive + description: + name: source_gen + url: "https://pub.dartlang.org" + source: hosted + version: "0.9.4+6" source_span: dependency: transitive description: name: source_span url: "https://pub.dartlang.org" source: hosted - version: "1.5.4" + version: "1.5.5" stack_trace: dependency: transitive description: @@ -113,14 +414,21 @@ packages: name: stream_channel url: "https://pub.dartlang.org" source: hosted - version: "1.6.8" + version: "2.0.0" + stream_transform: + dependency: transitive + description: + name: stream_transform + url: "https://pub.dartlang.org" + source: hosted + version: "0.0.19" string_scanner: dependency: transitive description: name: string_scanner url: "https://pub.dartlang.org" source: hosted - version: "1.0.4" + version: "1.0.5" term_glyph: dependency: transitive description: @@ -134,7 +442,14 @@ packages: name: test_api url: "https://pub.dartlang.org" source: hosted - version: "0.2.2" + version: "0.2.5" + timing: + dependency: transitive + description: + name: timing + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.1+2" typed_data: dependency: transitive description: @@ -149,6 +464,27 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.0.8" + watcher: + dependency: transitive + description: + name: watcher + url: "https://pub.dartlang.org" + source: hosted + version: "0.9.7+12" + web_socket_channel: + dependency: transitive + description: + name: web_socket_channel + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.0" + yaml: + dependency: transitive + description: + name: yaml + url: "https://pub.dartlang.org" + source: hosted + version: "2.2.0" sdks: - dart: ">=2.1.0 <3.0.0" - flutter: ">=0.1.4 <2.0.0" + dart: ">=2.3.0 <3.0.0" + flutter: ">=1.5.8 <2.0.0" diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 8698db64..14a4afdf 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -4,7 +4,7 @@ description: A new Flutter project. version: 1.0.0+1 environment: - sdk: ">=2.0.0-dev.68.0 <3.0.0" + sdk: ">=2.2.2 <3.0.0" dependencies: flutter: @@ -12,10 +12,16 @@ dependencies: flutter_hooks: path: ../ shared_preferences: ^0.4.3 + built_value: ^6.0.0 + built_collection: '>=2.0.0 <5.0.0' + provider: 3.1.0+1 dev_dependencies: flutter_test: sdk: flutter + built_value_generator: ^6.0.0 + build_runner: ^1.0.0 + flutter: uses-material-design: true diff --git a/example/test/widget_test.dart b/example/test/widget_test.dart new file mode 100644 index 00000000..747db1da --- /dev/null +++ b/example/test/widget_test.dart @@ -0,0 +1,30 @@ +// This is a basic Flutter widget test. +// +// To perform an interaction with a widget in your test, use the WidgetTester +// utility that Flutter provides. For example, you can send tap and scroll +// gestures. You can also use WidgetTester to find child widgets in the widget +// tree, read text, and verify that the values of widget properties are correct. + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'package:example/main.dart'; + +void main() { + testWidgets('Counter increments smoke test', (WidgetTester tester) async { + // Build our app and trigger a frame. + await tester.pumpWidget(MyApp()); + + // Verify that our counter starts at 0. + expect(find.text('0'), findsOneWidget); + expect(find.text('1'), findsNothing); + + // Tap the '+' icon and trigger a frame. + await tester.tap(find.byIcon(Icons.add)); + await tester.pump(); + + // Verify that our counter has incremented. + expect(find.text('0'), findsNothing); + expect(find.text('1'), findsOneWidget); + }); +} From 60b424f115dfd6b68dc83a19848e158cf7d59ec6 Mon Sep 17 00:00:00 2001 From: smiLLe Date: Thu, 31 Oct 2019 17:50:46 +0100 Subject: [PATCH 084/384] removed unused widget test --- example/test/widget_test.dart | 30 ------------------------------ 1 file changed, 30 deletions(-) delete mode 100644 example/test/widget_test.dart diff --git a/example/test/widget_test.dart b/example/test/widget_test.dart deleted file mode 100644 index 747db1da..00000000 --- a/example/test/widget_test.dart +++ /dev/null @@ -1,30 +0,0 @@ -// This is a basic Flutter widget test. -// -// To perform an interaction with a widget in your test, use the WidgetTester -// utility that Flutter provides. For example, you can send tap and scroll -// gestures. You can also use WidgetTester to find child widgets in the widget -// tree, read text, and verify that the values of widget properties are correct. - -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; - -import 'package:example/main.dart'; - -void main() { - testWidgets('Counter increments smoke test', (WidgetTester tester) async { - // Build our app and trigger a frame. - await tester.pumpWidget(MyApp()); - - // Verify that our counter starts at 0. - expect(find.text('0'), findsOneWidget); - expect(find.text('1'), findsNothing); - - // Tap the '+' icon and trigger a frame. - await tester.tap(find.byIcon(Icons.add)); - await tester.pump(); - - // Verify that our counter has incremented. - expect(find.text('0'), findsNothing); - expect(find.text('1'), findsOneWidget); - }); -} From 968a4d80076be18b8c2b16a937f39f38790b3d1d Mon Sep 17 00:00:00 2001 From: smiLLe Date: Thu, 31 Oct 2019 18:43:31 +0100 Subject: [PATCH 085/384] added more documentation, also moved state into redux file --- example/lib/star_wars/app_state.dart | 11 -- example/lib/star_wars/hooks.dart | 2 +- example/lib/star_wars/models.dart | 23 +++- example/lib/star_wars/models.g.dart | 127 +----------------- example/lib/star_wars/planet_list.dart | 1 - example/lib/star_wars/redux.dart | 23 +++- .../{app_state.g.dart => redux.g.dart} | 2 +- example/lib/star_wars/star_wars_api.dart | 2 - 8 files changed, 42 insertions(+), 149 deletions(-) delete mode 100644 example/lib/star_wars/app_state.dart rename example/lib/star_wars/{app_state.g.dart => redux.g.dart} (99%) diff --git a/example/lib/star_wars/app_state.dart b/example/lib/star_wars/app_state.dart deleted file mode 100644 index 67805852..00000000 --- a/example/lib/star_wars/app_state.dart +++ /dev/null @@ -1,11 +0,0 @@ -import 'package:built_value/built_value.dart'; -import 'package:flutter_hooks_gallery/star_wars/models.dart'; - -part 'app_state.g.dart'; - -abstract class AppState implements Built { - PlanetPageModel get planetPage; - - AppState._(); - factory AppState([void Function(AppStateBuilder) updates]) = _$AppState; -} diff --git a/example/lib/star_wars/hooks.dart b/example/lib/star_wars/hooks.dart index 63b75f1e..d724697e 100644 --- a/example/lib/star_wars/hooks.dart +++ b/example/lib/star_wars/hooks.dart @@ -1,9 +1,9 @@ import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:flutter_hooks_gallery/star_wars/app_state.dart'; import 'package:flutter_hooks_gallery/star_wars/redux.dart'; import 'package:flutter_hooks_gallery/star_wars/star_wars_api.dart'; import 'package:provider/provider.dart'; +/// fetch planets and update store typedef FetchAndDispatchPlanets = Future Function(String); /// return the redux store created by [useReducer} diff --git a/example/lib/star_wars/models.dart b/example/lib/star_wars/models.dart index aa15bb88..73d5e7b7 100644 --- a/example/lib/star_wars/models.dart +++ b/example/lib/star_wars/models.dart @@ -1,5 +1,3 @@ -// ignore_for_file: public_member_api_docs - import 'package:built_collection/built_collection.dart'; import 'package:built_value/built_value.dart'; import 'package:built_value/serializer.dart'; @@ -11,36 +9,47 @@ part 'models.g.dart'; PlanetPageModel, PlanetModel, ]) + +/// json serializer to build models final Serializers serializers = (_$serializers.toBuilder()..addPlugin(StandardJsonPlugin())).build(); +/// equals one page abstract class PlanetPageModel implements Built { + /// serialize the model static Serializer get serializer => _$planetPageModelSerializer; + + /// url to next page @nullable String get next; + /// url to prev page @nullable String get previous; + /// all planets BuiltList get results; PlanetPageModel._(); + + /// default factory factory PlanetPageModel([void Function(PlanetPageModelBuilder) updates]) = _$PlanetPageModel; } +/// equals one planet abstract class PlanetModel implements Built { + /// serialize the model static Serializer get serializer => _$planetModelSerializer; + + /// planet name String get name; - String get diameter; - String get climate; - String get terrain; - String get population; - String get url; PlanetModel._(); + + /// default factory factory PlanetModel([void Function(PlanetModelBuilder) updates]) = _$PlanetModel; } diff --git a/example/lib/star_wars/models.g.dart b/example/lib/star_wars/models.g.dart index 9e16c720..fdd9262f 100644 --- a/example/lib/star_wars/models.g.dart +++ b/example/lib/star_wars/models.g.dart @@ -93,20 +93,6 @@ class _$PlanetModelSerializer implements StructuredSerializer { final result = [ 'name', serializers.serialize(object.name, specifiedType: const FullType(String)), - 'diameter', - serializers.serialize(object.diameter, - specifiedType: const FullType(String)), - 'climate', - serializers.serialize(object.climate, - specifiedType: const FullType(String)), - 'terrain', - serializers.serialize(object.terrain, - specifiedType: const FullType(String)), - 'population', - serializers.serialize(object.population, - specifiedType: const FullType(String)), - 'url', - serializers.serialize(object.url, specifiedType: const FullType(String)), ]; return result; @@ -127,26 +113,6 @@ class _$PlanetModelSerializer implements StructuredSerializer { result.name = serializers.deserialize(value, specifiedType: const FullType(String)) as String; break; - case 'diameter': - result.diameter = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; - break; - case 'climate': - result.climate = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; - break; - case 'terrain': - result.terrain = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; - break; - case 'population': - result.population = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; - break; - case 'url': - result.url = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; - break; } } @@ -272,46 +238,14 @@ class PlanetPageModelBuilder class _$PlanetModel extends PlanetModel { @override final String name; - @override - final String diameter; - @override - final String climate; - @override - final String terrain; - @override - final String population; - @override - final String url; factory _$PlanetModel([void Function(PlanetModelBuilder) updates]) => (new PlanetModelBuilder()..update(updates)).build(); - _$PlanetModel._( - {this.name, - this.diameter, - this.climate, - this.terrain, - this.population, - this.url}) - : super._() { + _$PlanetModel._({this.name}) : super._() { if (name == null) { throw new BuiltValueNullFieldError('PlanetModel', 'name'); } - if (diameter == null) { - throw new BuiltValueNullFieldError('PlanetModel', 'diameter'); - } - if (climate == null) { - throw new BuiltValueNullFieldError('PlanetModel', 'climate'); - } - if (terrain == null) { - throw new BuiltValueNullFieldError('PlanetModel', 'terrain'); - } - if (population == null) { - throw new BuiltValueNullFieldError('PlanetModel', 'population'); - } - if (url == null) { - throw new BuiltValueNullFieldError('PlanetModel', 'url'); - } } @override @@ -324,36 +258,17 @@ class _$PlanetModel extends PlanetModel { @override bool operator ==(Object other) { if (identical(other, this)) return true; - return other is PlanetModel && - name == other.name && - diameter == other.diameter && - climate == other.climate && - terrain == other.terrain && - population == other.population && - url == other.url; + return other is PlanetModel && name == other.name; } @override int get hashCode { - return $jf($jc( - $jc( - $jc( - $jc($jc($jc(0, name.hashCode), diameter.hashCode), - climate.hashCode), - terrain.hashCode), - population.hashCode), - url.hashCode)); + return $jf($jc(0, name.hashCode)); } @override String toString() { - return (newBuiltValueToStringHelper('PlanetModel') - ..add('name', name) - ..add('diameter', diameter) - ..add('climate', climate) - ..add('terrain', terrain) - ..add('population', population) - ..add('url', url)) + return (newBuiltValueToStringHelper('PlanetModel')..add('name', name)) .toString(); } } @@ -365,36 +280,11 @@ class PlanetModelBuilder implements Builder { String get name => _$this._name; set name(String name) => _$this._name = name; - String _diameter; - String get diameter => _$this._diameter; - set diameter(String diameter) => _$this._diameter = diameter; - - String _climate; - String get climate => _$this._climate; - set climate(String climate) => _$this._climate = climate; - - String _terrain; - String get terrain => _$this._terrain; - set terrain(String terrain) => _$this._terrain = terrain; - - String _population; - String get population => _$this._population; - set population(String population) => _$this._population = population; - - String _url; - String get url => _$this._url; - set url(String url) => _$this._url = url; - PlanetModelBuilder(); PlanetModelBuilder get _$this { if (_$v != null) { _name = _$v.name; - _diameter = _$v.diameter; - _climate = _$v.climate; - _terrain = _$v.terrain; - _population = _$v.population; - _url = _$v.url; _$v = null; } return this; @@ -415,14 +305,7 @@ class PlanetModelBuilder implements Builder { @override _$PlanetModel build() { - final _$result = _$v ?? - new _$PlanetModel._( - name: name, - diameter: diameter, - climate: climate, - terrain: terrain, - population: population, - url: url); + final _$result = _$v ?? new _$PlanetModel._(name: name); replace(_$result); return _$result; } diff --git a/example/lib/star_wars/planet_list.dart b/example/lib/star_wars/planet_list.dart index 30946b06..93aa9413 100644 --- a/example/lib/star_wars/planet_list.dart +++ b/example/lib/star_wars/planet_list.dart @@ -1,6 +1,5 @@ import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:flutter_hooks_gallery/star_wars/app_state.dart'; import 'package:flutter_hooks_gallery/star_wars/hooks.dart'; import 'package:flutter_hooks_gallery/star_wars/redux.dart'; import 'package:flutter_hooks_gallery/star_wars/star_wars_api.dart'; diff --git a/example/lib/star_wars/redux.dart b/example/lib/star_wars/redux.dart index 15865fd0..38773f1f 100644 --- a/example/lib/star_wars/redux.dart +++ b/example/lib/star_wars/redux.dart @@ -1,17 +1,32 @@ -// ignore_for_file: public_member_api_docs - -import 'package:flutter_hooks_gallery/star_wars/app_state.dart'; +import 'package:built_value/built_value.dart'; import 'package:flutter_hooks_gallery/star_wars/models.dart'; +part 'redux.g.dart'; + +/// Actions base class abstract class ReduxAction {} +/// Action to set the planet page class SetPlanetPageAction extends ReduxAction { + /// payload final PlanetPageModel page; + /// constructor SetPlanetPageAction(this.page); } -/// reducer that is used by [useReducer] to create the redux store +/// state of the redux store +abstract class AppState implements Built { + /// current planet page + PlanetPageModel get planetPage; + + AppState._(); + + /// default factory + factory AppState([void Function(AppStateBuilder) updates]) = _$AppState; +} + +/// reducer that is used by useReducer to create the redux store AppState reducer(S state, A action) { final b = state.toBuilder(); if (action is SetPlanetPageAction) { diff --git a/example/lib/star_wars/app_state.g.dart b/example/lib/star_wars/redux.g.dart similarity index 99% rename from example/lib/star_wars/app_state.g.dart rename to example/lib/star_wars/redux.g.dart index 0e4c9e52..0cf4ddf2 100644 --- a/example/lib/star_wars/app_state.g.dart +++ b/example/lib/star_wars/redux.g.dart @@ -1,6 +1,6 @@ // GENERATED CODE - DO NOT MODIFY BY HAND -part of 'app_state.dart'; +part of 'redux.dart'; // ************************************************************************** // BuiltValueGenerator diff --git a/example/lib/star_wars/star_wars_api.dart b/example/lib/star_wars/star_wars_api.dart index dc7cecf0..b81a802d 100644 --- a/example/lib/star_wars/star_wars_api.dart +++ b/example/lib/star_wars/star_wars_api.dart @@ -1,5 +1,3 @@ -// ignore_for_file: public_member_api_docs - import 'dart:convert'; import 'package:flutter_hooks_gallery/star_wars/models.dart'; From 792f2d26252713dd5b7bc6db17f51a0a7c808118 Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Sat, 9 Nov 2019 22:55:17 +0100 Subject: [PATCH 086/384] Update Changelog/Readme --- CHANGELOG.md | 4 +++ README.md | 9 ++--- example/pubspec.lock | 79 +++++++++++++++++++++++++++++++++++--------- pubspec.lock | 65 +++++++++++++++++++++++++++++++----- pubspec.yaml | 2 +- 5 files changed, 131 insertions(+), 28 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e9e6d115..2314d014 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.7.0: + +- Added `useTextEditingController`, thanks to simolus3! + ## 0.6.1: - Added `useReassemble` hook, thanks to @SahandAkbarzadeh diff --git a/README.md b/README.md index 96f21e04..b9d3235a 100644 --- a/README.md +++ b/README.md @@ -328,7 +328,8 @@ They will take care of creating/updating/disposing an object. A series of hooks with no particular theme. -| name | description | -| --------------------------------------------------------------------------------------------------------- | ------------------------------------------------------ | -| [useReducer](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useReducer.html) | An alternative to `useState` for more complex states. | -| [usePrevious](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/usePrevious.html) | Returns the previous argument called to [usePrevious]. | +| name | description | +| ----------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------ | +| [useReducer](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useReducer.html) | An alternative to `useState` for more complex states. | +| [usePrevious](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/usePrevious.html) | Returns the previous argument called to [usePrevious]. | +| [useTextEditingController](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useTextEditingController.html) | Create a `TextEditingController` | diff --git a/example/pubspec.lock b/example/pubspec.lock index 01000bce..6a9174ec 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -1,20 +1,34 @@ # Generated by pub -# See https://www.dartlang.org/tools/pub/glossary#lockfile +# See https://dart.dev/tools/pub/glossary#lockfile packages: + archive: + dependency: transitive + description: + name: archive + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.10" + args: + dependency: transitive + description: + name: args + url: "https://pub.dartlang.org" + source: hosted + version: "1.5.2" async: dependency: transitive description: name: async url: "https://pub.dartlang.org" source: hosted - version: "2.0.8" + version: "2.3.0" boolean_selector: dependency: transitive description: name: boolean_selector url: "https://pub.dartlang.org" source: hosted - version: "1.0.4" + version: "1.0.5" charcode: dependency: transitive description: @@ -29,6 +43,20 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.14.11" + convert: + dependency: transitive + description: + name: convert + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.1" + crypto: + dependency: transitive + description: + name: crypto + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.3" flutter: dependency: "direct main" description: flutter @@ -40,47 +68,61 @@ packages: path: ".." relative: true source: path - version: "0.4.0" + version: "0.7.0" flutter_test: dependency: "direct dev" description: flutter source: sdk version: "0.0.0" + image: + dependency: transitive + description: + name: image + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.4" matcher: dependency: transitive description: name: matcher url: "https://pub.dartlang.org" source: hosted - version: "0.12.3+1" + version: "0.12.5" meta: dependency: transitive description: name: meta url: "https://pub.dartlang.org" source: hosted - version: "1.1.6" + version: "1.1.7" path: dependency: transitive description: name: path url: "https://pub.dartlang.org" source: hosted - version: "1.6.2" + version: "1.6.4" pedantic: dependency: transitive description: name: pedantic url: "https://pub.dartlang.org" source: hosted - version: "1.4.0" + version: "1.8.0+1" + petitparser: + dependency: transitive + description: + name: petitparser + url: "https://pub.dartlang.org" + source: hosted + version: "2.4.0" quiver: dependency: transitive description: name: quiver url: "https://pub.dartlang.org" source: hosted - version: "2.0.1" + version: "2.0.5" shared_preferences: dependency: "direct main" description: @@ -99,7 +141,7 @@ packages: name: source_span url: "https://pub.dartlang.org" source: hosted - version: "1.5.4" + version: "1.5.5" stack_trace: dependency: transitive description: @@ -113,14 +155,14 @@ packages: name: stream_channel url: "https://pub.dartlang.org" source: hosted - version: "1.6.8" + version: "2.0.0" string_scanner: dependency: transitive description: name: string_scanner url: "https://pub.dartlang.org" source: hosted - version: "1.0.4" + version: "1.0.5" term_glyph: dependency: transitive description: @@ -134,7 +176,7 @@ packages: name: test_api url: "https://pub.dartlang.org" source: hosted - version: "0.2.2" + version: "0.2.5" typed_data: dependency: transitive description: @@ -149,6 +191,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.0.8" + xml: + dependency: transitive + description: + name: xml + url: "https://pub.dartlang.org" + source: hosted + version: "3.5.0" sdks: - dart: ">=2.1.0 <3.0.0" - flutter: ">=0.1.4 <2.0.0" + dart: ">=2.4.0 <3.0.0" + flutter: ">=1.5.8 <2.0.0" diff --git a/pubspec.lock b/pubspec.lock index 53bffccd..54b7b900 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1,20 +1,34 @@ # Generated by pub # See https://dart.dev/tools/pub/glossary#lockfile packages: + archive: + dependency: transitive + description: + name: archive + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.10" + args: + dependency: transitive + description: + name: args + url: "https://pub.dartlang.org" + source: hosted + version: "1.5.2" async: dependency: transitive description: name: async url: "https://pub.dartlang.org" source: hosted - version: "2.2.0" + version: "2.3.0" boolean_selector: dependency: transitive description: name: boolean_selector url: "https://pub.dartlang.org" source: hosted - version: "1.0.4" + version: "1.0.5" charcode: dependency: transitive description: @@ -29,6 +43,20 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.14.11" + convert: + dependency: transitive + description: + name: convert + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.1" + crypto: + dependency: transitive + description: + name: crypto + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.3" flutter: dependency: "direct main" description: flutter @@ -39,6 +67,13 @@ packages: description: flutter source: sdk version: "0.0.0" + image: + dependency: transitive + description: + name: image + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.4" matcher: dependency: transitive description: @@ -52,7 +87,7 @@ packages: name: meta url: "https://pub.dartlang.org" source: hosted - version: "1.1.6" + version: "1.1.7" mockito: dependency: "direct dev" description: @@ -66,21 +101,28 @@ packages: name: path url: "https://pub.dartlang.org" source: hosted - version: "1.6.2" + version: "1.6.4" pedantic: dependency: "direct dev" description: name: pedantic url: "https://pub.dartlang.org" source: hosted - version: "1.7.0" + version: "1.8.0+1" + petitparser: + dependency: transitive + description: + name: petitparser + url: "https://pub.dartlang.org" + source: hosted + version: "2.4.0" quiver: dependency: transitive description: name: quiver url: "https://pub.dartlang.org" source: hosted - version: "2.0.3" + version: "2.0.5" sky_engine: dependency: transitive description: flutter @@ -113,7 +155,7 @@ packages: name: string_scanner url: "https://pub.dartlang.org" source: hosted - version: "1.0.4" + version: "1.0.5" term_glyph: dependency: transitive description: @@ -142,6 +184,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.0.8" + xml: + dependency: transitive + description: + name: xml + url: "https://pub.dartlang.org" + source: hosted + version: "3.5.0" sdks: - dart: ">=2.2.2 <3.0.0" + dart: ">=2.4.0 <3.0.0" flutter: ">=1.5.8 <2.0.0" diff --git a/pubspec.yaml b/pubspec.yaml index 24c9dda2..925b04c8 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -3,7 +3,7 @@ description: A flutter implementation of React hooks. It adds a new kind of widg homepage: https://github.com/rrousselGit/flutter_hooks author: Remi Rousselet -version: 0.6.1 +version: 0.7.0 environment: sdk: ">=2.0.0 <3.0.0" From 61bb0884e244c04ef63654d5257a35454ca47ce9 Mon Sep 17 00:00:00 2001 From: smiLLe Date: Tue, 26 Nov 2019 18:47:24 +0100 Subject: [PATCH 087/384] feedback - removed useless custom hooks - added/changed comments - moved constructors to top - added new class PlanetHandler to handle async stuff - refactored lots of widget code - updated redux actions/reducer --- example/lib/main.dart | 4 +- example/lib/star_wars/hooks.dart | 36 ---- example/lib/star_wars/models.dart | 27 ++- example/lib/star_wars/planet_list.dart | 150 ----------------- example/lib/star_wars/planet_screen.dart | 206 +++++++++++++++++++++++ example/lib/star_wars/redux.dart | 53 +++++- example/lib/star_wars/redux.g.dart | 40 ++++- 7 files changed, 302 insertions(+), 214 deletions(-) delete mode 100644 example/lib/star_wars/hooks.dart delete mode 100644 example/lib/star_wars/planet_list.dart create mode 100644 example/lib/star_wars/planet_screen.dart diff --git a/example/lib/main.dart b/example/lib/main.dart index d3baa12e..96f2385c 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -1,6 +1,6 @@ // ignore_for_file: omit_local_variable_types import 'package:flutter/material.dart'; -import 'package:flutter_hooks_gallery/star_wars/planet_list.dart'; +import 'package:flutter_hooks_gallery/star_wars/planet_screen.dart'; import 'package:flutter_hooks_gallery/use_effect.dart'; import 'package:flutter_hooks_gallery/use_state.dart'; import 'package:flutter_hooks_gallery/use_stream.dart'; @@ -34,7 +34,7 @@ class HooksGalleryApp extends StatelessWidget { ), _GalleryItem( title: 'Star Wars Planets', - builder: (context) => PlanetList(), + builder: (context) => PlanetScreen(), ) ]), ), diff --git a/example/lib/star_wars/hooks.dart b/example/lib/star_wars/hooks.dart deleted file mode 100644 index d724697e..00000000 --- a/example/lib/star_wars/hooks.dart +++ /dev/null @@ -1,36 +0,0 @@ -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:flutter_hooks_gallery/star_wars/redux.dart'; -import 'package:flutter_hooks_gallery/star_wars/star_wars_api.dart'; -import 'package:provider/provider.dart'; - -/// fetch planets and update store -typedef FetchAndDispatchPlanets = Future Function(String); - -/// return the redux store created by [useReducer} -/// We use [Provider] to retrieve the redux store. -Store useAppStore() { - final context = useContext(); - return Provider.of>(context); -} - -/// return [AppState] hold by redux store. -/// We use [Provider] to retrieve the [AppState]. -/// This will also rebuild whenever the [AppState] has been changed -AppState useAppState() { - final context = useContext(); - return Provider.of(context); -} - -/// return star wars api. -/// We use [Provider] to retrieve the [StarWarsApi]. -StarWarsApi useStarWarsApi() { - final context = useContext(); - return Provider.of(context); -} - -/// "middleware" to load data and update state -/// We use [Provider] to retrieve the [FetchAndDispatchPlanets] "middleware". -FetchAndDispatchPlanets useFetchAndDispatchPlanets() { - final context = useContext(); - return Provider.of(context); -} diff --git a/example/lib/star_wars/models.dart b/example/lib/star_wars/models.dart index 73d5e7b7..d260f153 100644 --- a/example/lib/star_wars/models.dart +++ b/example/lib/star_wars/models.dart @@ -5,18 +5,23 @@ import 'package:built_value/standard_json_plugin.dart'; part 'models.g.dart'; +/// json serializer to build models @SerializersFor([ PlanetPageModel, PlanetModel, ]) - -/// json serializer to build models final Serializers serializers = (_$serializers.toBuilder()..addPlugin(StandardJsonPlugin())).build(); /// equals one page abstract class PlanetPageModel implements Built { + PlanetPageModel._(); + + /// default factory + factory PlanetPageModel([void Function(PlanetPageModelBuilder) updates]) = + _$PlanetPageModel; + /// serialize the model static Serializer get serializer => _$planetPageModelSerializer; @@ -31,25 +36,19 @@ abstract class PlanetPageModel /// all planets BuiltList get results; - - PlanetPageModel._(); - - /// default factory - factory PlanetPageModel([void Function(PlanetPageModelBuilder) updates]) = - _$PlanetPageModel; } /// equals one planet abstract class PlanetModel implements Built { - /// serialize the model - static Serializer get serializer => _$planetModelSerializer; - - /// planet name - String get name; - PlanetModel._(); /// default factory factory PlanetModel([void Function(PlanetModelBuilder) updates]) = _$PlanetModel; + + /// serialize the model + static Serializer get serializer => _$planetModelSerializer; + + /// planet name + String get name; } diff --git a/example/lib/star_wars/planet_list.dart b/example/lib/star_wars/planet_list.dart deleted file mode 100644 index 93aa9413..00000000 --- a/example/lib/star_wars/planet_list.dart +++ /dev/null @@ -1,150 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:flutter_hooks_gallery/star_wars/hooks.dart'; -import 'package:flutter_hooks_gallery/star_wars/redux.dart'; -import 'package:flutter_hooks_gallery/star_wars/star_wars_api.dart'; -import 'package:provider/provider.dart'; - -/// This example will load, show and let you navigate through all star wars -/// planets. -/// -/// It will demonstrate on how to use [Provider] and redux ([useReducer]) -class PlanetList extends HookWidget { - @override - Widget build(BuildContext context) { - /// create single instance of Star Wars Api - final api = useMemoized(() => StarWarsApi()); - - /// create single instance of redux store - final store = useReducer( - reducer, - initialState: AppState(), - ); - - return Scaffold( - appBar: AppBar( - title: const Text( - 'Star Wars Planets', - ), - ), - - /// provide star wars api instance, - /// redux store instance - /// and redux state down the widget tree. - body: MultiProvider( - providers: [ - Provider.value(value: api), - Provider.value(value: store), - Provider.value(value: store.state), - ], - child: HookBuilder( - builder: (context) { - final state = useAppState(); - final store = useAppStore(); - final isLoadingState = useState(false); - - final fetchAndDispatchPlanets = - useMemoized( - () => ([String url]) async { - isLoadingState.value = true; - final page = await api.getPlanets(url); - store.dispatch(SetPlanetPageAction(page)); - isLoadingState.value = false; - }, - [store], - ); - - final buttonAlignment = useMemoized( - () { - if (null == state.planetPage.previous) { - return MainAxisAlignment.end; - } - if (null == state.planetPage.next) { - return MainAxisAlignment.start; - } - return MainAxisAlignment.spaceBetween; - }, - [state], - ); - - /// load the first planet page but only on the first build - useEffect( - () { - fetchAndDispatchPlanets(null); - return () {}; - }, - const [], - ); - - return Provider.value( - value: fetchAndDispatchPlanets, - child: CustomScrollView( - slivers: [ - if (isLoadingState.value) - SliverToBoxAdapter( - child: Center(child: const CircularProgressIndicator()), - ), - if (!isLoadingState.value) - SliverToBoxAdapter( - child: Row( - mainAxisAlignment: buttonAlignment, - children: [ - if (null != state.planetPage.previous) - _LoadPageButton( - next: false, - ), - if (null != state.planetPage.next) - _LoadPageButton( - next: true, - ) - ], - ), - ), - if (!isLoadingState.value && - state.planetPage.results.isNotEmpty) - _List(), - ], - ), - ); - }, - ), - ), - ); - } -} - -class _LoadPageButton extends HookWidget { - final bool next; - - _LoadPageButton({this.next = true}) : assert(next != null); - - @override - Widget build(BuildContext context) { - final state = useAppState(); - final fetchAndDispatch = useFetchAndDispatchPlanets(); - - return RaisedButton( - child: next ? const Text('Next Page') : const Text('Prev Page'), - onPressed: () async { - final url = next ? state.planetPage.next : state.planetPage.previous; - await fetchAndDispatch(url); - }, - ); - } -} - -class _List extends HookWidget { - @override - Widget build(BuildContext context) { - final state = useAppState(); - return SliverList( - delegate: SliverChildListDelegate( - state.planetPage.results - .map((planet) => ListTile( - title: Text(planet.name), - )) - .toList(), - ), - ); - } -} diff --git a/example/lib/star_wars/planet_screen.dart b/example/lib/star_wars/planet_screen.dart new file mode 100644 index 00000000..fe756139 --- /dev/null +++ b/example/lib/star_wars/planet_screen.dart @@ -0,0 +1,206 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:flutter_hooks_gallery/star_wars/redux.dart'; +import 'package:flutter_hooks_gallery/star_wars/star_wars_api.dart'; +import 'package:provider/provider.dart'; + +/// This handler will take care of async api interactions +/// and updating the store afterwards. +class _PlanetHandler { + /// constructor + _PlanetHandler(this._store, this._starWarsApi); + + final Store _store; + final StarWarsApi _starWarsApi; + + /// This will load all planets and will dispatch all necessary actions + /// on the redux store. + Future fetchAndDispatch([String url]) async { + _store.dispatch(FetchPlanetPageActionStart()); + try { + final page = await _starWarsApi.getPlanets(url); + _store.dispatch(FetchPlanetPageActionSuccess(page)); + } catch (e) { + _store.dispatch(FetchPlanetPageActionError('Error loading Planets')); + } + } +} + +/// This example will load, show and let you navigate through all star wars +/// planets. +/// +/// It will demonstrate on how to use [Provider] and [useReducer] +class PlanetScreen extends HookWidget { + @override + Widget build(BuildContext context) { + final api = useMemoized(() => StarWarsApi()); + + final store = useReducer( + reducer, + initialState: AppState(), + ); + + final planetHandler = useMemoized( + () => _PlanetHandler(store, api), + [store, api], + ); + + /// load the first planet page but only once + useEffect( + () { + planetHandler.fetchAndDispatch(null); + return () {}; + }, + [planetHandler], + ); + + return MultiProvider( + providers: [ + Provider.value(value: planetHandler), + Provider.value(value: store.state), + ], + child: Scaffold( + appBar: AppBar( + title: const Text( + 'Star Wars Planets', + ), + ), + body: _PlanetScreenBody(), + ), + ); + } +} + +class _PlanetScreenBody extends StatelessWidget { + @override + Widget build(BuildContext context) { + return Consumer( + builder: (context, state, _) { + if (null != state.errorFetchingPlanets) { + return _Error( + errorMsg: state.errorFetchingPlanets, + ); + } + + return CustomScrollView( + slivers: [ + if (state.isFetchingPlanets) + SliverFillViewport( + delegate: SliverChildListDelegate.fixed( + [ + Center( + child: const CircularProgressIndicator(), + ), + ], + ), + ), + if (!state.isFetchingPlanets) + SliverToBoxAdapter( + child: HookBuilder(builder: (context) { + final buttonAlignment = useMemoized( + () { + if (null == state.planetPage.previous) { + return MainAxisAlignment.end; + } + if (null == state.planetPage.next) { + return MainAxisAlignment.start; + } + return MainAxisAlignment.spaceBetween; + }, + [state], + ); + + return Row( + mainAxisAlignment: buttonAlignment, + children: [ + if (null != state.planetPage.previous) + _LoadPageButton( + next: false, + ), + if (null != state.planetPage.next) + _LoadPageButton( + next: true, + ) + ], + ); + }), + ), + if (!state.isFetchingPlanets && state.planetPage.results.isNotEmpty) + _PlanetList(), + ], + ); + }, + ); + } +} + +class _Error extends StatelessWidget { + final String errorMsg; + + const _Error({Key key, this.errorMsg}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Consumer<_PlanetHandler>(builder: (context, handler, _) { + return Container( + alignment: Alignment.center, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + if (null != errorMsg) Text(errorMsg), + RaisedButton( + color: Colors.redAccent, + child: const Text('Try again'), + onPressed: () async { + await handler.fetchAndDispatch(); + }, + ), + ], + ), + ); + }); + } +} + +class _LoadPageButton extends HookWidget { + _LoadPageButton({this.next = true}) : assert(next != null); + + final bool next; + + @override + Widget build(BuildContext context) { + return Consumer<_PlanetHandler>( + builder: (context, handler, _) { + return Consumer( + builder: (context, state, _) { + return RaisedButton( + child: next ? const Text('Next Page') : const Text('Prev Page'), + onPressed: () async { + final url = + next ? state.planetPage.next : state.planetPage.previous; + await handler.fetchAndDispatch(url); + }, + ); + }, + ); + }, + ); + } +} + +class _PlanetList extends HookWidget { + @override + Widget build(BuildContext context) { + return Consumer(builder: (context, state, _) { + return SliverList( + delegate: SliverChildListDelegate( + [ + for (var planet in state.planetPage.results) + ListTile( + title: Text(planet.name), + ) + ], + )); + }); + } +} diff --git a/example/lib/star_wars/redux.dart b/example/lib/star_wars/redux.dart index 38773f1f..e1706def 100644 --- a/example/lib/star_wars/redux.dart +++ b/example/lib/star_wars/redux.dart @@ -6,31 +6,68 @@ part 'redux.g.dart'; /// Actions base class abstract class ReduxAction {} +/// Action that updates state to show that we are loading planets +class FetchPlanetPageActionStart extends ReduxAction {} + +/// Action that updates state to show that we are loading planets +class FetchPlanetPageActionError extends ReduxAction { + /// Message that should be displayed in the UI + final String errorMsg; + + /// constructor + FetchPlanetPageActionError(this.errorMsg); +} + /// Action to set the planet page -class SetPlanetPageAction extends ReduxAction { +class FetchPlanetPageActionSuccess extends ReduxAction { /// payload final PlanetPageModel page; /// constructor - SetPlanetPageAction(this.page); + FetchPlanetPageActionSuccess(this.page); } /// state of the redux store abstract class AppState implements Built { - /// current planet page - PlanetPageModel get planetPage; - AppState._(); /// default factory - factory AppState([void Function(AppStateBuilder) updates]) = _$AppState; + factory AppState([void Function(AppStateBuilder) updates]) => + _$AppState((u) => u + ..isFetchingPlanets = false + ..update(updates)); + + /// are we currently loading planets + bool get isFetchingPlanets; + + /// will be set if loading planets failed. This is an error message + @nullable + String get errorFetchingPlanets; + + /// current planet page + PlanetPageModel get planetPage; } /// reducer that is used by useReducer to create the redux store AppState reducer(S state, A action) { final b = state.toBuilder(); - if (action is SetPlanetPageAction) { - b.planetPage.replace(action.page); + if (action is FetchPlanetPageActionStart) { + b + ..isFetchingPlanets = true + ..planetPage = PlanetPageModelBuilder() + ..errorFetchingPlanets = null; + } + + if (action is FetchPlanetPageActionError) { + b + ..isFetchingPlanets = false + ..errorFetchingPlanets = action.errorMsg; + } + + if (action is FetchPlanetPageActionSuccess) { + b + ..isFetchingPlanets = false + ..planetPage.replace(action.page); } return b.build(); diff --git a/example/lib/star_wars/redux.g.dart b/example/lib/star_wars/redux.g.dart index 0cf4ddf2..c7e04e0f 100644 --- a/example/lib/star_wars/redux.g.dart +++ b/example/lib/star_wars/redux.g.dart @@ -7,13 +7,22 @@ part of 'redux.dart'; // ************************************************************************** class _$AppState extends AppState { + @override + final bool isFetchingPlanets; + @override + final String errorFetchingPlanets; @override final PlanetPageModel planetPage; factory _$AppState([void Function(AppStateBuilder) updates]) => (new AppStateBuilder()..update(updates)).build(); - _$AppState._({this.planetPage}) : super._() { + _$AppState._( + {this.isFetchingPlanets, this.errorFetchingPlanets, this.planetPage}) + : super._() { + if (isFetchingPlanets == null) { + throw new BuiltValueNullFieldError('AppState', 'isFetchingPlanets'); + } if (planetPage == null) { throw new BuiltValueNullFieldError('AppState', 'planetPage'); } @@ -29,17 +38,24 @@ class _$AppState extends AppState { @override bool operator ==(Object other) { if (identical(other, this)) return true; - return other is AppState && planetPage == other.planetPage; + return other is AppState && + isFetchingPlanets == other.isFetchingPlanets && + errorFetchingPlanets == other.errorFetchingPlanets && + planetPage == other.planetPage; } @override int get hashCode { - return $jf($jc(0, planetPage.hashCode)); + return $jf($jc( + $jc($jc(0, isFetchingPlanets.hashCode), errorFetchingPlanets.hashCode), + planetPage.hashCode)); } @override String toString() { return (newBuiltValueToStringHelper('AppState') + ..add('isFetchingPlanets', isFetchingPlanets) + ..add('errorFetchingPlanets', errorFetchingPlanets) ..add('planetPage', planetPage)) .toString(); } @@ -48,6 +64,16 @@ class _$AppState extends AppState { class AppStateBuilder implements Builder { _$AppState _$v; + bool _isFetchingPlanets; + bool get isFetchingPlanets => _$this._isFetchingPlanets; + set isFetchingPlanets(bool isFetchingPlanets) => + _$this._isFetchingPlanets = isFetchingPlanets; + + String _errorFetchingPlanets; + String get errorFetchingPlanets => _$this._errorFetchingPlanets; + set errorFetchingPlanets(String errorFetchingPlanets) => + _$this._errorFetchingPlanets = errorFetchingPlanets; + PlanetPageModelBuilder _planetPage; PlanetPageModelBuilder get planetPage => _$this._planetPage ??= new PlanetPageModelBuilder(); @@ -58,6 +84,8 @@ class AppStateBuilder implements Builder { AppStateBuilder get _$this { if (_$v != null) { + _isFetchingPlanets = _$v.isFetchingPlanets; + _errorFetchingPlanets = _$v.errorFetchingPlanets; _planetPage = _$v.planetPage?.toBuilder(); _$v = null; } @@ -81,7 +109,11 @@ class AppStateBuilder implements Builder { _$AppState build() { _$AppState _$result; try { - _$result = _$v ?? new _$AppState._(planetPage: planetPage.build()); + _$result = _$v ?? + new _$AppState._( + isFetchingPlanets: isFetchingPlanets, + errorFetchingPlanets: errorFetchingPlanets, + planetPage: planetPage.build()); } catch (_) { String _$failedField; try { From c98835ac09d4c02dbe5f486aec8c98df4f1a0153 Mon Sep 17 00:00:00 2001 From: smiLLe Date: Sun, 1 Dec 2019 18:18:17 +0100 Subject: [PATCH 088/384] feedback... - Removed CustomScrollView. Instead added Stack. Also changed PlanetList to ListView. - Removed Consumer in favor of Provider.of() for less indentation. - Removed some Captain Obvious comments :) - Reversed comparisons are no longer reversed --- example/lib/star_wars/models.dart | 10 +- example/lib/star_wars/planet_screen.dart | 215 ++++++++++++----------- 2 files changed, 110 insertions(+), 115 deletions(-) diff --git a/example/lib/star_wars/models.dart b/example/lib/star_wars/models.dart index d260f153..6cb32683 100644 --- a/example/lib/star_wars/models.dart +++ b/example/lib/star_wars/models.dart @@ -1,3 +1,5 @@ +// ignore_for_file: public_member_api_docs + import 'package:built_collection/built_collection.dart'; import 'package:built_value/built_value.dart'; import 'package:built_value/serializer.dart'; @@ -18,23 +20,18 @@ abstract class PlanetPageModel implements Built { PlanetPageModel._(); - /// default factory factory PlanetPageModel([void Function(PlanetPageModelBuilder) updates]) = _$PlanetPageModel; - /// serialize the model static Serializer get serializer => _$planetPageModelSerializer; - /// url to next page @nullable String get next; - /// url to prev page @nullable String get previous; - /// all planets BuiltList get results; } @@ -42,13 +39,10 @@ abstract class PlanetPageModel abstract class PlanetModel implements Built { PlanetModel._(); - /// default factory factory PlanetModel([void Function(PlanetModelBuilder) updates]) = _$PlanetModel; - /// serialize the model static Serializer get serializer => _$planetModelSerializer; - /// planet name String get name; } diff --git a/example/lib/star_wars/planet_screen.dart b/example/lib/star_wars/planet_screen.dart index fe756139..1b314171 100644 --- a/example/lib/star_wars/planet_screen.dart +++ b/example/lib/star_wars/planet_screen.dart @@ -41,17 +41,13 @@ class PlanetScreen extends HookWidget { ); final planetHandler = useMemoized( - () => _PlanetHandler(store, api), - [store, api], - ); - - /// load the first planet page but only once - useEffect( () { - planetHandler.fetchAndDispatch(null); - return () {}; + /// Create planet handler and load the first page. + /// The first page will only be loaded once, after the handler was created + final handler = _PlanetHandler(store, api)..fetchAndDispatch(null); + return handler; }, - [planetHandler], + [store, api], ); return MultiProvider( @@ -71,65 +67,46 @@ class PlanetScreen extends HookWidget { } } -class _PlanetScreenBody extends StatelessWidget { +class _PlanetScreenBody extends HookWidget { @override Widget build(BuildContext context) { - return Consumer( - builder: (context, state, _) { - if (null != state.errorFetchingPlanets) { - return _Error( - errorMsg: state.errorFetchingPlanets, - ); - } - - return CustomScrollView( - slivers: [ - if (state.isFetchingPlanets) - SliverFillViewport( - delegate: SliverChildListDelegate.fixed( - [ - Center( - child: const CircularProgressIndicator(), - ), - ], - ), - ), - if (!state.isFetchingPlanets) - SliverToBoxAdapter( - child: HookBuilder(builder: (context) { - final buttonAlignment = useMemoized( - () { - if (null == state.planetPage.previous) { - return MainAxisAlignment.end; - } - if (null == state.planetPage.next) { - return MainAxisAlignment.start; - } - return MainAxisAlignment.spaceBetween; - }, - [state], - ); - - return Row( - mainAxisAlignment: buttonAlignment, - children: [ - if (null != state.planetPage.previous) - _LoadPageButton( - next: false, - ), - if (null != state.planetPage.next) - _LoadPageButton( - next: true, - ) - ], - ); - }), - ), - if (!state.isFetchingPlanets && state.planetPage.results.isNotEmpty) - _PlanetList(), - ], - ); - }, + final state = Provider.of(context); + final index = useMemoized(() { + if (state.isFetchingPlanets) { + return 0; + } + + if (state.planetPage.results.isEmpty) { + return 1; + } + + if (state.errorFetchingPlanets != null) { + return 2; + } + + return 3; + }, [ + state.isFetchingPlanets, + state.planetPage.results.isEmpty, + state.errorFetchingPlanets + ]); + + return IndexedStack( + children: [ + Container( + alignment: Alignment.center, + child: const CircularProgressIndicator(), + ), + Container( + alignment: Alignment.center, + child: const Text('No planets found'), + ), + _Error( + errorMsg: state.errorFetchingPlanets, + ), + _PlanetList(), + ], + index: index, ); } } @@ -141,24 +118,23 @@ class _Error extends StatelessWidget { @override Widget build(BuildContext context) { - return Consumer<_PlanetHandler>(builder: (context, handler, _) { - return Container( - alignment: Alignment.center, - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - if (null != errorMsg) Text(errorMsg), - RaisedButton( - color: Colors.redAccent, - child: const Text('Try again'), - onPressed: () async { - await handler.fetchAndDispatch(); - }, - ), - ], - ), - ); - }); + final handler = Provider.of<_PlanetHandler>(context); + return Container( + alignment: Alignment.center, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + if (errorMsg != null) Text(errorMsg), + RaisedButton( + color: Colors.redAccent, + child: const Text('Try again'), + onPressed: () async { + await handler.fetchAndDispatch(); + }, + ), + ], + ), + ); } } @@ -169,20 +145,13 @@ class _LoadPageButton extends HookWidget { @override Widget build(BuildContext context) { - return Consumer<_PlanetHandler>( - builder: (context, handler, _) { - return Consumer( - builder: (context, state, _) { - return RaisedButton( - child: next ? const Text('Next Page') : const Text('Prev Page'), - onPressed: () async { - final url = - next ? state.planetPage.next : state.planetPage.previous; - await handler.fetchAndDispatch(url); - }, - ); - }, - ); + final handler = Provider.of<_PlanetHandler>(context); + final state = Provider.of(context); + return RaisedButton( + child: next ? const Text('Next Page') : const Text('Prev Page'), + onPressed: () async { + final url = next ? state.planetPage.next : state.planetPage.previous; + await handler.fetchAndDispatch(url); }, ); } @@ -191,16 +160,48 @@ class _LoadPageButton extends HookWidget { class _PlanetList extends HookWidget { @override Widget build(BuildContext context) { - return Consumer(builder: (context, state, _) { - return SliverList( - delegate: SliverChildListDelegate( - [ - for (var planet in state.planetPage.results) - ListTile( - title: Text(planet.name), - ) + final state = Provider.of(context); + return ListView.builder( + itemCount: 1 + state.planetPage.results.length, + itemBuilder: (context, index) { + if (index == 0) { + return _PlanetListHeader(); + } + + final planet = state.planetPage.results[index - 1]; + return ListTile( + title: Text(planet.name), + ); + }, + ); + } +} + +class _PlanetListHeader extends StatelessWidget { + @override + Widget build(BuildContext context) { + final state = Provider.of(context); + return HookBuilder(builder: (context) { + final buttonAlignment = useMemoized( + () { + if (state.planetPage.previous == null) { + return MainAxisAlignment.end; + } + if (state.planetPage.next == null) { + return MainAxisAlignment.start; + } + return MainAxisAlignment.spaceBetween; + }, + [state], + ); + + return Row( + mainAxisAlignment: buttonAlignment, + children: [ + if (state.planetPage.previous != null) _LoadPageButton(next: false), + if (state.planetPage.next != null) _LoadPageButton(next: true) ], - )); + ); }); } } From fd3f3dc558d0f52806e26ab1531b2e0567f42ae0 Mon Sep 17 00:00:00 2001 From: Michael Date: Tue, 10 Dec 2019 22:42:08 +0100 Subject: [PATCH 089/384] Update example/lib/star_wars/planet_screen.dart Co-Authored-By: Remi Rousselet --- example/lib/star_wars/planet_screen.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/example/lib/star_wars/planet_screen.dart b/example/lib/star_wars/planet_screen.dart index 1b314171..697fedd0 100644 --- a/example/lib/star_wars/planet_screen.dart +++ b/example/lib/star_wars/planet_screen.dart @@ -7,7 +7,6 @@ import 'package:provider/provider.dart'; /// This handler will take care of async api interactions /// and updating the store afterwards. class _PlanetHandler { - /// constructor _PlanetHandler(this._store, this._starWarsApi); final Store _store; From 73d24dc6c0881b433cb8aa9265bf049884a38dae Mon Sep 17 00:00:00 2001 From: Michael Date: Tue, 10 Dec 2019 22:42:21 +0100 Subject: [PATCH 090/384] Update example/lib/star_wars/planet_screen.dart Co-Authored-By: Remi Rousselet --- example/lib/star_wars/planet_screen.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/example/lib/star_wars/planet_screen.dart b/example/lib/star_wars/planet_screen.dart index 697fedd0..8581547d 100644 --- a/example/lib/star_wars/planet_screen.dart +++ b/example/lib/star_wars/planet_screen.dart @@ -43,7 +43,7 @@ class PlanetScreen extends HookWidget { () { /// Create planet handler and load the first page. /// The first page will only be loaded once, after the handler was created - final handler = _PlanetHandler(store, api)..fetchAndDispatch(null); + return _PlanetHandler(store, api)..fetchAndDispatch(); return handler; }, [store, api], From 90a623e34d99504cc8b67bffab2268f9d631cf23 Mon Sep 17 00:00:00 2001 From: Michael Date: Tue, 10 Dec 2019 22:43:02 +0100 Subject: [PATCH 091/384] Update example/lib/star_wars/planet_screen.dart Co-Authored-By: Remi Rousselet --- example/lib/star_wars/planet_screen.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/example/lib/star_wars/planet_screen.dart b/example/lib/star_wars/planet_screen.dart index 8581547d..dd467e8e 100644 --- a/example/lib/star_wars/planet_screen.dart +++ b/example/lib/star_wars/planet_screen.dart @@ -60,7 +60,7 @@ class PlanetScreen extends HookWidget { 'Star Wars Planets', ), ), - body: _PlanetScreenBody(), + body: const _PlanetScreenBody(), ), ); } From 8e143e0a041e13d5f2023bd623eec49c925e46f6 Mon Sep 17 00:00:00 2001 From: Michael Date: Tue, 10 Dec 2019 22:43:16 +0100 Subject: [PATCH 092/384] Update example/lib/star_wars/planet_screen.dart Co-Authored-By: Remi Rousselet --- example/lib/star_wars/planet_screen.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/example/lib/star_wars/planet_screen.dart b/example/lib/star_wars/planet_screen.dart index dd467e8e..3202892c 100644 --- a/example/lib/star_wars/planet_screen.dart +++ b/example/lib/star_wars/planet_screen.dart @@ -92,7 +92,7 @@ class _PlanetScreenBody extends HookWidget { return IndexedStack( children: [ - Container( + Center( alignment: Alignment.center, child: const CircularProgressIndicator(), ), From 6cab5880c91fa59fb46422498149ec08e562d68e Mon Sep 17 00:00:00 2001 From: Michael Date: Tue, 10 Dec 2019 22:43:26 +0100 Subject: [PATCH 093/384] Update example/lib/star_wars/planet_screen.dart Co-Authored-By: Remi Rousselet --- example/lib/star_wars/planet_screen.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/example/lib/star_wars/planet_screen.dart b/example/lib/star_wars/planet_screen.dart index 3202892c..e49f8a40 100644 --- a/example/lib/star_wars/planet_screen.dart +++ b/example/lib/star_wars/planet_screen.dart @@ -96,7 +96,7 @@ class _PlanetScreenBody extends HookWidget { alignment: Alignment.center, child: const CircularProgressIndicator(), ), - Container( + Center( alignment: Alignment.center, child: const Text('No planets found'), ), From 77fe07d90c0818be03e14ce53c1faa44b6bbe057 Mon Sep 17 00:00:00 2001 From: Michael Date: Tue, 10 Dec 2019 22:43:52 +0100 Subject: [PATCH 094/384] Update example/lib/star_wars/planet_screen.dart Co-Authored-By: Remi Rousselet --- example/lib/star_wars/planet_screen.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/example/lib/star_wars/planet_screen.dart b/example/lib/star_wars/planet_screen.dart index e49f8a40..474d66c7 100644 --- a/example/lib/star_wars/planet_screen.dart +++ b/example/lib/star_wars/planet_screen.dart @@ -117,7 +117,6 @@ class _Error extends StatelessWidget { @override Widget build(BuildContext context) { - final handler = Provider.of<_PlanetHandler>(context); return Container( alignment: Alignment.center, child: Column( From 0d953e74a3f9ad8bbb4c36fc8d728c9fabc179cf Mon Sep 17 00:00:00 2001 From: Michael Date: Tue, 10 Dec 2019 22:44:08 +0100 Subject: [PATCH 095/384] Update example/lib/star_wars/planet_screen.dart Co-Authored-By: Remi Rousselet --- example/lib/star_wars/planet_screen.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/example/lib/star_wars/planet_screen.dart b/example/lib/star_wars/planet_screen.dart index 474d66c7..756f03c5 100644 --- a/example/lib/star_wars/planet_screen.dart +++ b/example/lib/star_wars/planet_screen.dart @@ -117,7 +117,7 @@ class _Error extends StatelessWidget { @override Widget build(BuildContext context) { - return Container( + return Center( alignment: Alignment.center, child: Column( mainAxisAlignment: MainAxisAlignment.center, From 9ba425f4d0c6879685a540ccc43c9b46cf5978f0 Mon Sep 17 00:00:00 2001 From: Michael Date: Tue, 10 Dec 2019 22:44:28 +0100 Subject: [PATCH 096/384] Update example/lib/star_wars/planet_screen.dart Co-Authored-By: Remi Rousselet --- example/lib/star_wars/planet_screen.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/example/lib/star_wars/planet_screen.dart b/example/lib/star_wars/planet_screen.dart index 756f03c5..32c012a0 100644 --- a/example/lib/star_wars/planet_screen.dart +++ b/example/lib/star_wars/planet_screen.dart @@ -149,7 +149,7 @@ class _LoadPageButton extends HookWidget { child: next ? const Text('Next Page') : const Text('Prev Page'), onPressed: () async { final url = next ? state.planetPage.next : state.planetPage.previous; - await handler.fetchAndDispatch(url); + Provider.of<_PlanetHandler>(context, listen: false).fetchAndDispatch(url); }, ); } From c5922bea83f2c8f786499849dd4e94ca70f8dd8c Mon Sep 17 00:00:00 2001 From: Michael Date: Tue, 10 Dec 2019 22:44:43 +0100 Subject: [PATCH 097/384] Update example/lib/star_wars/planet_screen.dart Co-Authored-By: Remi Rousselet --- example/lib/star_wars/planet_screen.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/example/lib/star_wars/planet_screen.dart b/example/lib/star_wars/planet_screen.dart index 32c012a0..8da719a7 100644 --- a/example/lib/star_wars/planet_screen.dart +++ b/example/lib/star_wars/planet_screen.dart @@ -143,7 +143,6 @@ class _LoadPageButton extends HookWidget { @override Widget build(BuildContext context) { - final handler = Provider.of<_PlanetHandler>(context); final state = Provider.of(context); return RaisedButton( child: next ? const Text('Next Page') : const Text('Prev Page'), From aa4a13bd3bcd8c7653163e1c00af64d95b192db5 Mon Sep 17 00:00:00 2001 From: smiLLe Date: Wed, 11 Dec 2019 14:43:00 +0100 Subject: [PATCH 098/384] feedback --- example/lib/star_wars/planet_screen.dart | 121 +++++++++-------------- 1 file changed, 45 insertions(+), 76 deletions(-) diff --git a/example/lib/star_wars/planet_screen.dart b/example/lib/star_wars/planet_screen.dart index 8da719a7..276dd36a 100644 --- a/example/lib/star_wars/planet_screen.dart +++ b/example/lib/star_wars/planet_screen.dart @@ -44,7 +44,6 @@ class PlanetScreen extends HookWidget { /// Create planet handler and load the first page. /// The first page will only be loaded once, after the handler was created return _PlanetHandler(store, api)..fetchAndDispatch(); - return handler; }, [store, api], ); @@ -67,46 +66,25 @@ class PlanetScreen extends HookWidget { } class _PlanetScreenBody extends HookWidget { + const _PlanetScreenBody(); + @override Widget build(BuildContext context) { final state = Provider.of(context); - final index = useMemoized(() { - if (state.isFetchingPlanets) { - return 0; - } - - if (state.planetPage.results.isEmpty) { - return 1; - } - - if (state.errorFetchingPlanets != null) { - return 2; - } - - return 3; - }, [ - state.isFetchingPlanets, - state.planetPage.results.isEmpty, - state.errorFetchingPlanets - ]); - - return IndexedStack( - children: [ - Center( - alignment: Alignment.center, - child: const CircularProgressIndicator(), - ), - Center( - alignment: Alignment.center, - child: const Text('No planets found'), - ), - _Error( + + if (state.isFetchingPlanets) { + return const Center(child: CircularProgressIndicator()); + } else if (state.planetPage.results.isEmpty) { + return const Center(child: Text('No planets found')); + } else if (state.errorFetchingPlanets != null) { + return Center( + child: _Error( errorMsg: state.errorFetchingPlanets, ), - _PlanetList(), - ], - index: index, - ); + ); + } else { + return _PlanetList(); + } } } @@ -117,21 +95,19 @@ class _Error extends StatelessWidget { @override Widget build(BuildContext context) { - return Center( - alignment: Alignment.center, - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - if (errorMsg != null) Text(errorMsg), - RaisedButton( - color: Colors.redAccent, - child: const Text('Try again'), - onPressed: () async { - await handler.fetchAndDispatch(); - }, - ), - ], - ), + return Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + if (errorMsg != null) Text(errorMsg), + RaisedButton( + color: Colors.redAccent, + child: const Text('Try again'), + onPressed: () async { + await Provider.of<_PlanetHandler>(context, listen: false) + .fetchAndDispatch(); + }, + ), + ], ); } } @@ -148,7 +124,8 @@ class _LoadPageButton extends HookWidget { child: next ? const Text('Next Page') : const Text('Prev Page'), onPressed: () async { final url = next ? state.planetPage.next : state.planetPage.previous; - Provider.of<_PlanetHandler>(context, listen: false).fetchAndDispatch(url); + await Provider.of<_PlanetHandler>(context, listen: false) + .fetchAndDispatch(url); }, ); } @@ -166,9 +143,7 @@ class _PlanetList extends HookWidget { } final planet = state.planetPage.results[index - 1]; - return ListTile( - title: Text(planet.name), - ); + return ListTile(title: Text(planet.name)); }, ); } @@ -178,27 +153,21 @@ class _PlanetListHeader extends StatelessWidget { @override Widget build(BuildContext context) { final state = Provider.of(context); - return HookBuilder(builder: (context) { - final buttonAlignment = useMemoized( - () { - if (state.planetPage.previous == null) { - return MainAxisAlignment.end; - } - if (state.planetPage.next == null) { - return MainAxisAlignment.start; - } - return MainAxisAlignment.spaceBetween; - }, - [state], - ); + MainAxisAlignment buttonAlignment; + if (state.planetPage.previous == null) { + buttonAlignment = MainAxisAlignment.end; + } else if (state.planetPage.next == null) { + buttonAlignment = MainAxisAlignment.start; + } else { + buttonAlignment = MainAxisAlignment.spaceBetween; + } - return Row( - mainAxisAlignment: buttonAlignment, - children: [ - if (state.planetPage.previous != null) _LoadPageButton(next: false), - if (state.planetPage.next != null) _LoadPageButton(next: true) - ], - ); - }); + return Row( + mainAxisAlignment: buttonAlignment, + children: [ + if (state.planetPage.previous != null) _LoadPageButton(next: false), + if (state.planetPage.next != null) _LoadPageButton(next: true) + ], + ); } } From 8c49c4cd7c31fc2a910cb591fd734104ea516d0e Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Mon, 27 Jan 2020 17:55:53 +0000 Subject: [PATCH 099/384] upgrade --- example/.flutter-plugins-dependencies | 1 + example/analysis_options.yaml | 11 ++++++++ example/lib/use_stream.dart | 3 +- example/pubspec.lock | 40 +++++++++++++++++++++++---- example/pubspec.yaml | 2 +- lib/src/framework.dart | 4 +-- lib/src/text_controller.dart | 2 +- pubspec.lock | 13 ++++----- pubspec.yaml | 3 +- test/hook_widget_test.dart | 4 +-- test/use_reassemble_test.dart | 1 - 11 files changed, 61 insertions(+), 23 deletions(-) create mode 100644 example/.flutter-plugins-dependencies create mode 100644 example/analysis_options.yaml diff --git a/example/.flutter-plugins-dependencies b/example/.flutter-plugins-dependencies new file mode 100644 index 00000000..98ac26cf --- /dev/null +++ b/example/.flutter-plugins-dependencies @@ -0,0 +1 @@ +{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"shared_preferences","path":"/Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/shared_preferences-0.4.3/","dependencies":[]}],"android":[{"name":"shared_preferences","path":"/Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/shared_preferences-0.4.3/","dependencies":[]}],"macos":[],"linux":[],"windows":[],"web":[]},"dependencyGraph":[{"name":"shared_preferences","dependencies":[]}],"date_created":"2020-01-27 17:55:50.387137","version":"1.14.5-pre.36"} \ No newline at end of file diff --git a/example/analysis_options.yaml b/example/analysis_options.yaml new file mode 100644 index 00000000..b6d5666f --- /dev/null +++ b/example/analysis_options.yaml @@ -0,0 +1,11 @@ +include: ../analysis_options.yaml +analyzer: + strong-mode: + implicit-casts: false + implicit-dynamic: false + errors: + todo: error + include_file_not_found: ignore +linter: + rules: + public_member_api_docs: false diff --git a/example/lib/use_stream.dart b/example/lib/use_stream.dart index b54fb121..91c95ab0 100644 --- a/example/lib/use_stream.dart +++ b/example/lib/use_stream.dart @@ -24,7 +24,8 @@ class UseStreamExample extends StatelessWidget { // Future) the first time this builder function is invoked without // recreating it on each subsequent build! final stream = useMemoized( - () => Stream.periodic(Duration(seconds: 1), (i) => i + 1), + () => Stream.periodic( + const Duration(seconds: 1), (i) => i + 1), ); // Next, invoke the `useStream` hook to listen for updates to the // Stream. This triggers a rebuild whenever a new value is emitted. diff --git a/example/pubspec.lock b/example/pubspec.lock index 739634bc..fddefee8 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -15,6 +15,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.2.1" + archive: + dependency: transitive + description: + name: archive + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.11" args: dependency: transitive description: @@ -28,7 +35,7 @@ packages: name: async url: "https://pub.dartlang.org" source: hosted - version: "2.3.0" + version: "2.4.0" boolean_selector: dependency: transitive description: @@ -228,6 +235,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "3.1.3" + image: + dependency: transitive + description: + name: image + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.4" io: dependency: transitive description: @@ -269,14 +283,14 @@ packages: name: matcher url: "https://pub.dartlang.org" source: hosted - version: "0.12.5" + version: "0.12.6" meta: dependency: transitive description: name: meta url: "https://pub.dartlang.org" source: hosted - version: "1.1.7" + version: "1.1.8" mime: dependency: transitive description: @@ -326,6 +340,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.8.0+1" + petitparser: + dependency: transitive + description: + name: petitparser + url: "https://pub.dartlang.org" + source: hosted + version: "2.4.0" pool: dependency: transitive description: @@ -442,7 +463,7 @@ packages: name: test_api url: "https://pub.dartlang.org" source: hosted - version: "0.2.5" + version: "0.2.11" timing: dependency: transitive description: @@ -478,6 +499,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.1.0" + xml: + dependency: transitive + description: + name: xml + url: "https://pub.dartlang.org" + source: hosted + version: "3.5.0" yaml: dependency: transitive description: @@ -486,5 +514,5 @@ packages: source: hosted version: "2.2.0" sdks: - dart: ">=2.3.0 <3.0.0" - flutter: ">=1.5.8 <2.0.0" + dart: ">=2.7.0 <3.0.0" + flutter: ">=0.1.4 <2.0.0" diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 14a4afdf..9e520f93 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -4,7 +4,7 @@ description: A new Flutter project. version: 1.0.0+1 environment: - sdk: ">=2.2.2 <3.0.0" + sdk: ">=2.7.0 <3.0.0" dependencies: flutter: diff --git a/lib/src/framework.dart b/lib/src/framework.dart index 7a09d990..1ed842e9 100644 --- a/lib/src/framework.dart +++ b/lib/src/framework.dart @@ -308,10 +308,10 @@ This may happen if the call to `Hook.use` is made under some condition. List get debugHooks => List.unmodifiable(_hooks); @override - InheritedWidget inheritFromWidgetOfExactType(Type targetType, + T dependOnInheritedWidgetOfExactType( {Object aspect}) { assert(!_debugIsInitHook); - return super.inheritFromWidgetOfExactType(targetType, aspect: aspect); + return super.dependOnInheritedWidgetOfExactType(aspect: aspect); } @override diff --git a/lib/src/text_controller.dart b/lib/src/text_controller.dart index d9eb661a..eb8b33be 100644 --- a/lib/src/text_controller.dart +++ b/lib/src/text_controller.dart @@ -35,7 +35,7 @@ class _TextEditingControllerHookCreator { /// Changing the text or initial value after the widget has been built has no /// effect whatsoever. To update the value in a callback, for instance after a /// button was pressed, use the [TextEditingController.text] or -/// [TextEditingController.value] setters. To have the [TextEditingController] +/// [TextEditingController.text] setters. To have the [TextEditingController] /// reflect changing values, you can use [useEffect]. This example will update /// the [TextEditingController.text] whenever a provided [ValueListenable] /// changes: diff --git a/pubspec.lock b/pubspec.lock index 54b7b900..d1b672d9 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -7,7 +7,7 @@ packages: name: archive url: "https://pub.dartlang.org" source: hosted - version: "2.0.10" + version: "2.0.11" args: dependency: transitive description: @@ -21,7 +21,7 @@ packages: name: async url: "https://pub.dartlang.org" source: hosted - version: "2.3.0" + version: "2.4.0" boolean_selector: dependency: transitive description: @@ -80,14 +80,14 @@ packages: name: matcher url: "https://pub.dartlang.org" source: hosted - version: "0.12.5" + version: "0.12.6" meta: dependency: transitive description: name: meta url: "https://pub.dartlang.org" source: hosted - version: "1.1.7" + version: "1.1.8" mockito: dependency: "direct dev" description: @@ -169,7 +169,7 @@ packages: name: test_api url: "https://pub.dartlang.org" source: hosted - version: "0.2.5" + version: "0.2.11" typed_data: dependency: transitive description: @@ -192,5 +192,4 @@ packages: source: hosted version: "3.5.0" sdks: - dart: ">=2.4.0 <3.0.0" - flutter: ">=1.5.8 <2.0.0" + dart: ">=2.7.0 <3.0.0" diff --git a/pubspec.yaml b/pubspec.yaml index 925b04c8..ab958eca 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -6,8 +6,7 @@ author: Remi Rousselet version: 0.7.0 environment: - sdk: ">=2.0.0 <3.0.0" - flutter: ">=1.5.8 <2.0.0" + sdk: ">=2.7.0 <3.0.0" dependencies: flutter: diff --git a/test/hook_widget_test.dart b/test/hook_widget_test.dart index 5f91aec5..af17e898 100644 --- a/test/hook_widget_test.dart +++ b/test/hook_widget_test.dart @@ -12,7 +12,7 @@ class InheritedInitHook extends Hook { class InheritedInitHookState extends HookState { @override void initHook() { - context.inheritFromWidgetOfExactType(InheritedWidget); + context.dependOnInheritedWidgetOfExactType(); } @override @@ -69,7 +69,7 @@ void main() { (tester) async { when(build(any)).thenAnswer((invocation) { invocation.positionalArguments.first as BuildContext - ..inheritFromWidgetOfExactType(InheritedWidget); + ..dependOnInheritedWidgetOfExactType(); return null; }); diff --git a/test/use_reassemble_test.dart b/test/use_reassemble_test.dart index d3c59dd5..2bdd84d4 100644 --- a/test/use_reassemble_test.dart +++ b/test/use_reassemble_test.dart @@ -29,5 +29,4 @@ void main() { verify(reassemble()).called(1); verifyNoMoreInteractions(reassemble); }); - } From 7b052db47217bd7f1bff9e68957a2295318ffce1 Mon Sep 17 00:00:00 2001 From: Sahandevs Date: Wed, 29 Jan 2020 13:24:55 +0330 Subject: [PATCH 100/384] add deactivate to hookstate --- lib/src/framework.dart | 23 +++++++++++++++++++++++ test/hook_widget_test.dart | 3 +++ test/mock.dart | 10 ++++++++++ 3 files changed, 36 insertions(+) diff --git a/lib/src/framework.dart b/lib/src/framework.dart index 1ed842e9..8dd8081d 100644 --- a/lib/src/framework.dart +++ b/lib/src/framework.dart @@ -222,6 +222,9 @@ abstract class HookState> { @protected void didUpdateHook(T oldHook) {} + /// Equivalent of [State.deactivate] for [HookState] + void deactivate() {} + /// {@macro flutter.widgets.reassemble} /// /// In addition to this method being invoked, it is guaranteed that the @@ -354,6 +357,26 @@ This may happen if the call to `Hook.use` is made under some condition. } } + @override + void deactivate() { + super.deactivate(); + if (_hooks != null) { + for (final hook in _hooks) { + try { + hook.deactivate(); + } catch (exception, stack) { + FlutterError.reportError(FlutterErrorDetails( + exception: exception, + stack: stack, + library: 'hooks library', + context: + DiagnosticsNode.message('while deactivating ${hook.runtimeType}'), + )); + } + } + } + } + @override void reassemble() { super.reassemble(); diff --git a/test/hook_widget_test.dart b/test/hook_widget_test.dart index af17e898..48ca482d 100644 --- a/test/hook_widget_test.dart +++ b/test/hook_widget_test.dart @@ -22,6 +22,7 @@ class InheritedInitHookState extends HookState { void main() { final build = Func1(); final dispose = Func0(); + final deactivate = Func0(); final initHook = Func0(); final didUpdateHook = Func1(); final didBuild = Func0(); @@ -35,6 +36,7 @@ void main() { reassemble: reassemble.call, initHook: initHook.call, didBuild: didBuild, + deactivate: deactivate, ); void verifyNoMoreHookInteration() { @@ -50,6 +52,7 @@ void main() { reset(build); reset(didBuild); reset(dispose); + reset(deactivate); reset(initHook); reset(didUpdateHook); reset(reassemble); diff --git a/test/mock.dart b/test/mock.dart index e6f15107..135bf933 100644 --- a/test/mock.dart +++ b/test/mock.dart @@ -30,6 +30,7 @@ class HookTest extends Hook { final void Function() dispose; final void Function() didBuild; final void Function() initHook; + final void Function() deactivate; final void Function(HookTest previousHook) didUpdateHook; final void Function() reassemble; final HookStateTest Function() createStateFn; @@ -42,6 +43,7 @@ class HookTest extends Hook { this.reassemble, this.createStateFn, this.didBuild, + this.deactivate, List keys, }) : super(keys: keys); @@ -90,6 +92,14 @@ class HookStateTest extends HookState> { } } + @override + void deactivate() { + super.deactivate(); + if (hook.deactivate != null) { + hook.deactivate(); + } + } + @override R build(BuildContext context) { if (hook.build != null) { From f90c82988a1920c377b2d665bf453954635d0284 Mon Sep 17 00:00:00 2001 From: Sahandevs Date: Wed, 29 Jan 2020 13:33:17 +0330 Subject: [PATCH 101/384] implement useAutomaticKeepAliveClient --- lib/src/framework.dart | 2 +- lib/src/misc.dart | 67 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 68 insertions(+), 1 deletion(-) diff --git a/lib/src/framework.dart b/lib/src/framework.dart index 8dd8081d..684f3a9e 100644 --- a/lib/src/framework.dart +++ b/lib/src/framework.dart @@ -359,7 +359,6 @@ This may happen if the call to `Hook.use` is made under some condition. @override void deactivate() { - super.deactivate(); if (_hooks != null) { for (final hook in _hooks) { try { @@ -375,6 +374,7 @@ This may happen if the call to `Hook.use` is made under some condition. } } } + super.deactivate(); } @override diff --git a/lib/src/misc.dart b/lib/src/misc.dart index 5c34a1ae..1d14ecfe 100644 --- a/lib/src/misc.dart +++ b/lib/src/misc.dart @@ -143,3 +143,70 @@ class _ReassembleHookState extends HookState { @override void build(BuildContext context) {} } + +/// Allows subtrees to request to be kept alive in lazy lists. +/// +/// See also: +/// +/// * [AutomaticKeepAlive] +/// * [AutomaticKeepAliveClientMixin] +void useAutomaticKeepAliveClient({bool wantKeepAlive = true}) { + Hook.use(_AutomaticKeepAliveClientHook(wantKeepAlive)); +} + +class _AutomaticKeepAliveClientHook extends Hook { + final bool wantKeepAlive; + + _AutomaticKeepAliveClientHook(this.wantKeepAlive); + + @override + _AutomaticKeepAliveClientHookState createState() => + _AutomaticKeepAliveClientHookState(); +} + +class _AutomaticKeepAliveClientHookState + extends HookState { + KeepAliveHandle _keepAliveHandle; + + bool get wantKeepAlive => hook.wantKeepAlive; + + void _ensureKeepAlive() { + assert(_keepAliveHandle == null); + _keepAliveHandle = KeepAliveHandle(); + KeepAliveNotification(_keepAliveHandle).dispatch(context); + } + + void _releaseKeepAlive() { + _keepAliveHandle.release(); + _keepAliveHandle = null; + } + + /// Ensures that any [AutomaticKeepAlive] ancestors are in a good state, by + /// firing a [KeepAliveNotification] or triggering the [KeepAliveHandle] as + /// appropriate. + @protected + void updateKeepAlive() { + if (wantKeepAlive) { + if (_keepAliveHandle == null) _ensureKeepAlive(); + } else { + if (_keepAliveHandle != null) _releaseKeepAlive(); + } + } + + @override + initHook() { + super.initHook(); + if (wantKeepAlive) _ensureKeepAlive(); + } + + @override + void deactivate() { + if (_keepAliveHandle != null) _releaseKeepAlive(); + super.deactivate(); + } + + @override + void build(BuildContext context) { + if (wantKeepAlive && _keepAliveHandle == null) _ensureKeepAlive(); + } +} From f1f8910d81b94b656dd7d97fb3b19ab5b4f420eb Mon Sep 17 00:00:00 2001 From: Sahandevs Date: Wed, 29 Jan 2020 14:47:41 +0330 Subject: [PATCH 102/384] add tests for useAutomaticKeepAliveClient --- lib/src/misc.dart | 3 +- .../use_automatic_keep_alive_client_test.dart | 72 +++++++++++++++++++ 2 files changed, 74 insertions(+), 1 deletion(-) create mode 100644 test/use_automatic_keep_alive_client_test.dart diff --git a/lib/src/misc.dart b/lib/src/misc.dart index 1d14ecfe..7e847f5b 100644 --- a/lib/src/misc.dart +++ b/lib/src/misc.dart @@ -157,7 +157,8 @@ void useAutomaticKeepAliveClient({bool wantKeepAlive = true}) { class _AutomaticKeepAliveClientHook extends Hook { final bool wantKeepAlive; - _AutomaticKeepAliveClientHook(this.wantKeepAlive); + _AutomaticKeepAliveClientHook(this.wantKeepAlive) + : assert(wantKeepAlive != null); @override _AutomaticKeepAliveClientHookState createState() => diff --git a/test/use_automatic_keep_alive_client_test.dart b/test/use_automatic_keep_alive_client_test.dart new file mode 100644 index 00000000..8c285342 --- /dev/null +++ b/test/use_automatic_keep_alive_client_test.dart @@ -0,0 +1,72 @@ +import 'package:flutter/rendering.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:flutter_hooks/src/framework.dart'; +import 'package:flutter_test/flutter_test.dart'; + +class Leaf extends HookWidget { + final Widget child; + + const Leaf({Key key, this.child}) : super(key: key); + + @override + Widget build(BuildContext context) { + useAutomaticKeepAliveClient(wantKeepAlive: true); + return child; + } +} + +List generateList(Widget child) { + return List.generate( + 100, + (int index) { + final Widget result = Leaf( + key: getKey(index), + child: child, + ); + return result; + }, + growable: false, + ); +} + +Key getKey(int index) => Key('$index'); + +void main() { + testWidgets('AutomaticKeepAlive with ListView', (WidgetTester tester) async { + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: ListView( + addSemanticIndexes: false, + scrollDirection: Axis.vertical, + itemExtent: 12.3, + // about 50 widgets visible + cacheExtent: 0.0, + children: generateList(const Placeholder()), + ), + ), + ); + await tester.pump(); + + expect(find.byKey(getKey(0)), findsOneWidget); + expect(find.byKey(getKey(1)), findsOneWidget); + expect(find.byKey(getKey(30)), findsOneWidget); + expect(find.byKey(getKey(40)), findsOneWidget); + expect(find.byKey(getKey(60), skipOffstage: false), findsNothing); + await tester.drag(find.byType(ListView), const Offset(0.0, -300.0)); // down + await tester.pump(); + expect(find.byKey(getKey(60), skipOffstage: false), findsOneWidget); + expect(find.byKey(getKey(0), skipOffstage: false), findsOneWidget); + expect(find.byKey(getKey(1), skipOffstage: false), findsOneWidget); + expect(find.byKey(getKey(30), skipOffstage: false), findsOneWidget); + expect(find.byKey(getKey(40), skipOffstage: false), findsOneWidget); + await tester.drag(find.byType(ListView), const Offset(0.0, 300.0)); // top + await tester.pump(); + expect(find.byKey(getKey(60), skipOffstage: false), findsOneWidget); + expect(find.byKey(getKey(0), skipOffstage: false), findsOneWidget); + expect(find.byKey(getKey(1), skipOffstage: false), findsOneWidget); + expect(find.byKey(getKey(30), skipOffstage: false), findsOneWidget); + expect(find.byKey(getKey(40), skipOffstage: false), findsOneWidget); + }); +} From bc8e24841f6c2b5fd58c796d82636cb2a6c2c14d Mon Sep 17 00:00:00 2001 From: Sahandevs Date: Tue, 4 Feb 2020 16:06:14 +0330 Subject: [PATCH 103/384] remove useAutomaticKeepAliveClient --- lib/src/misc.dart | 68 ------------------ .../use_automatic_keep_alive_client_test.dart | 72 ------------------- 2 files changed, 140 deletions(-) delete mode 100644 test/use_automatic_keep_alive_client_test.dart diff --git a/lib/src/misc.dart b/lib/src/misc.dart index 7e847f5b..5c34a1ae 100644 --- a/lib/src/misc.dart +++ b/lib/src/misc.dart @@ -143,71 +143,3 @@ class _ReassembleHookState extends HookState { @override void build(BuildContext context) {} } - -/// Allows subtrees to request to be kept alive in lazy lists. -/// -/// See also: -/// -/// * [AutomaticKeepAlive] -/// * [AutomaticKeepAliveClientMixin] -void useAutomaticKeepAliveClient({bool wantKeepAlive = true}) { - Hook.use(_AutomaticKeepAliveClientHook(wantKeepAlive)); -} - -class _AutomaticKeepAliveClientHook extends Hook { - final bool wantKeepAlive; - - _AutomaticKeepAliveClientHook(this.wantKeepAlive) - : assert(wantKeepAlive != null); - - @override - _AutomaticKeepAliveClientHookState createState() => - _AutomaticKeepAliveClientHookState(); -} - -class _AutomaticKeepAliveClientHookState - extends HookState { - KeepAliveHandle _keepAliveHandle; - - bool get wantKeepAlive => hook.wantKeepAlive; - - void _ensureKeepAlive() { - assert(_keepAliveHandle == null); - _keepAliveHandle = KeepAliveHandle(); - KeepAliveNotification(_keepAliveHandle).dispatch(context); - } - - void _releaseKeepAlive() { - _keepAliveHandle.release(); - _keepAliveHandle = null; - } - - /// Ensures that any [AutomaticKeepAlive] ancestors are in a good state, by - /// firing a [KeepAliveNotification] or triggering the [KeepAliveHandle] as - /// appropriate. - @protected - void updateKeepAlive() { - if (wantKeepAlive) { - if (_keepAliveHandle == null) _ensureKeepAlive(); - } else { - if (_keepAliveHandle != null) _releaseKeepAlive(); - } - } - - @override - initHook() { - super.initHook(); - if (wantKeepAlive) _ensureKeepAlive(); - } - - @override - void deactivate() { - if (_keepAliveHandle != null) _releaseKeepAlive(); - super.deactivate(); - } - - @override - void build(BuildContext context) { - if (wantKeepAlive && _keepAliveHandle == null) _ensureKeepAlive(); - } -} diff --git a/test/use_automatic_keep_alive_client_test.dart b/test/use_automatic_keep_alive_client_test.dart deleted file mode 100644 index 8c285342..00000000 --- a/test/use_automatic_keep_alive_client_test.dart +++ /dev/null @@ -1,72 +0,0 @@ -import 'package:flutter/rendering.dart'; -import 'package:flutter/widgets.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:flutter_hooks/src/framework.dart'; -import 'package:flutter_test/flutter_test.dart'; - -class Leaf extends HookWidget { - final Widget child; - - const Leaf({Key key, this.child}) : super(key: key); - - @override - Widget build(BuildContext context) { - useAutomaticKeepAliveClient(wantKeepAlive: true); - return child; - } -} - -List generateList(Widget child) { - return List.generate( - 100, - (int index) { - final Widget result = Leaf( - key: getKey(index), - child: child, - ); - return result; - }, - growable: false, - ); -} - -Key getKey(int index) => Key('$index'); - -void main() { - testWidgets('AutomaticKeepAlive with ListView', (WidgetTester tester) async { - await tester.pumpWidget( - Directionality( - textDirection: TextDirection.ltr, - child: ListView( - addSemanticIndexes: false, - scrollDirection: Axis.vertical, - itemExtent: 12.3, - // about 50 widgets visible - cacheExtent: 0.0, - children: generateList(const Placeholder()), - ), - ), - ); - await tester.pump(); - - expect(find.byKey(getKey(0)), findsOneWidget); - expect(find.byKey(getKey(1)), findsOneWidget); - expect(find.byKey(getKey(30)), findsOneWidget); - expect(find.byKey(getKey(40)), findsOneWidget); - expect(find.byKey(getKey(60), skipOffstage: false), findsNothing); - await tester.drag(find.byType(ListView), const Offset(0.0, -300.0)); // down - await tester.pump(); - expect(find.byKey(getKey(60), skipOffstage: false), findsOneWidget); - expect(find.byKey(getKey(0), skipOffstage: false), findsOneWidget); - expect(find.byKey(getKey(1), skipOffstage: false), findsOneWidget); - expect(find.byKey(getKey(30), skipOffstage: false), findsOneWidget); - expect(find.byKey(getKey(40), skipOffstage: false), findsOneWidget); - await tester.drag(find.byType(ListView), const Offset(0.0, 300.0)); // top - await tester.pump(); - expect(find.byKey(getKey(60), skipOffstage: false), findsOneWidget); - expect(find.byKey(getKey(0), skipOffstage: false), findsOneWidget); - expect(find.byKey(getKey(1), skipOffstage: false), findsOneWidget); - expect(find.byKey(getKey(30), skipOffstage: false), findsOneWidget); - expect(find.byKey(getKey(40), skipOffstage: false), findsOneWidget); - }); -} From d3b7074a11831af50470fe5c5b7b4c0529f1c7cb Mon Sep 17 00:00:00 2001 From: Sahandevs Date: Tue, 4 Feb 2020 16:19:42 +0330 Subject: [PATCH 104/384] add test for deactivate --- test/hook_widget_test.dart | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/test/hook_widget_test.dart b/test/hook_widget_test.dart index 48ca482d..e7b227ff 100644 --- a/test/hook_widget_test.dart +++ b/test/hook_widget_test.dart @@ -58,6 +58,44 @@ void main() { reset(reassemble); }); + testWidgets('should call deactivate when removed from and inserted into another place', (tester) async { + final _key = GlobalKey(); + final state = ValueNotifier(false); + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.rtl, + child: ValueListenableBuilder( + valueListenable: state, + builder: (context, bool value, _) => Stack( + children: [ + HookBuilder( + key: value ? null : _key, + builder: (context) { + Hook.use(createHook()); + return Container(); + }, + ), + HookBuilder( + key: !value ? null : _key, + builder: (context) { + Hook.use(createHook()); + return Container(); + }, + ), + ] + ) + ), + ) + ); + await tester.pump(); + verifyNever(deactivate()); + state.value = true; + await tester.pump(); + verify(deactivate()).called(1); + await tester.pump(); + verifyNoMoreInteractions(deactivate); + }); + testWidgets('should not allow using inheritedwidgets inside initHook', (tester) async { await tester.pumpWidget(HookBuilder(builder: (_) { From bd005dd4b7b89be1ee02634229cb77cf8fc152cb Mon Sep 17 00:00:00 2001 From: Sahandevs Date: Tue, 4 Feb 2020 16:26:12 +0330 Subject: [PATCH 105/384] format framework.dart --- lib/src/framework.dart | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/src/framework.dart b/lib/src/framework.dart index 684f3a9e..0d68a56b 100644 --- a/lib/src/framework.dart +++ b/lib/src/framework.dart @@ -368,8 +368,9 @@ This may happen if the call to `Hook.use` is made under some condition. exception: exception, stack: stack, library: 'hooks library', - context: - DiagnosticsNode.message('while deactivating ${hook.runtimeType}'), + context: DiagnosticsNode.message( + 'while deactivating ${hook.runtimeType}', + ), )); } } From 54c636579bbb7b7d5a077fe0022b0e361589d994 Mon Sep 17 00:00:00 2001 From: Sahandevs Date: Tue, 4 Feb 2020 18:28:24 +0330 Subject: [PATCH 106/384] test all deactivate calls in "should call deactivate when removed from and inserted into another place" --- test/hook_widget_test.dart | 70 +++++++++++++++++++++----------------- 1 file changed, 39 insertions(+), 31 deletions(-) diff --git a/test/hook_widget_test.dart b/test/hook_widget_test.dart index e7b227ff..c1b6552e 100644 --- a/test/hook_widget_test.dart +++ b/test/hook_widget_test.dart @@ -58,42 +58,50 @@ void main() { reset(reassemble); }); - testWidgets('should call deactivate when removed from and inserted into another place', (tester) async { - final _key = GlobalKey(); + testWidgets( + 'should call deactivate when removed from and inserted into another place', + (tester) async { + final _key1 = GlobalKey(); + final _key2 = GlobalKey(); final state = ValueNotifier(false); - await tester.pumpWidget( - Directionality( - textDirection: TextDirection.rtl, - child: ValueListenableBuilder( - valueListenable: state, - builder: (context, bool value, _) => Stack( - children: [ - HookBuilder( - key: value ? null : _key, - builder: (context) { - Hook.use(createHook()); - return Container(); - }, - ), - HookBuilder( - key: !value ? null : _key, - builder: (context) { - Hook.use(createHook()); - return Container(); - }, - ), - ] - ) - ), - ) - ); + final deactivate1 = Func0(); + final deactivate2 = Func0(); + await tester.pumpWidget(Directionality( + textDirection: TextDirection.rtl, + child: ValueListenableBuilder( + valueListenable: state, + builder: (context, bool value, _) => Stack(children: [ + Container( + key: const Key('1'), + child: HookBuilder( + key: value ? _key2 : _key1, + builder: (context) { + Hook.use(HookTest(deactivate: deactivate1)); + return Container(); + }, + ), + ), + HookBuilder( + key: !value ? _key2 : _key1, + builder: (context) { + Hook.use(HookTest(deactivate: deactivate2)); + return Container(); + }, + ), + ])), + )); await tester.pump(); - verifyNever(deactivate()); + verifyNever(deactivate1()); + verifyNever(deactivate2()); state.value = true; await tester.pump(); - verify(deactivate()).called(1); + verifyInOrder([ + deactivate1.call(), + deactivate2.call(), + ]); await tester.pump(); - verifyNoMoreInteractions(deactivate); + verifyNoMoreInteractions(deactivate1); + verifyNoMoreInteractions(deactivate2); }); testWidgets('should not allow using inheritedwidgets inside initHook', From 6dd1cbad8a163e97c11b706073f223acd332e28c Mon Sep 17 00:00:00 2001 From: Sahandevs Date: Tue, 4 Feb 2020 19:05:55 +0330 Subject: [PATCH 107/384] add more test for deactivate lifecycle --- test/hook_widget_test.dart | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/test/hook_widget_test.dart b/test/hook_widget_test.dart index c1b6552e..bc12ceae 100644 --- a/test/hook_widget_test.dart +++ b/test/hook_widget_test.dart @@ -104,6 +104,42 @@ void main() { verifyNoMoreInteractions(deactivate2); }); + testWidgets('should call other deactivates even if one fails', + (tester) async { + final deactivate2 = Func0(); + final _key = GlobalKey(); + + when(didBuild.call()).thenThrow(42); + when(builder.call(any)).thenAnswer((invocation) { + Hook.use(createHook()); + Hook.use(HookTest(didBuild: deactivate2)); + return Container(); + }); + + await expectPump( + () => tester.pumpWidget(HookBuilder( + key: _key, + builder: builder.call, + )), + throwsA(42), + ); + + await expectPump( + () => tester.pumpWidget(Container( + child: HookBuilder( + key: _key, + builder: builder.call, + ), + )), + throwsA(42), + ); + + verifyInOrder([ + deactivate2.call(), + deactivate.call(), + ]); + }); + testWidgets('should not allow using inheritedwidgets inside initHook', (tester) async { await tester.pumpWidget(HookBuilder(builder: (_) { From 8271c4ab7b9f69505eb879bde78e826fa12b4876 Mon Sep 17 00:00:00 2001 From: Sahandevs Date: Tue, 4 Feb 2020 19:13:04 +0330 Subject: [PATCH 108/384] fix typo --- test/hook_widget_test.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/hook_widget_test.dart b/test/hook_widget_test.dart index bc12ceae..af4b0a98 100644 --- a/test/hook_widget_test.dart +++ b/test/hook_widget_test.dart @@ -112,7 +112,7 @@ void main() { when(didBuild.call()).thenThrow(42); when(builder.call(any)).thenAnswer((invocation) { Hook.use(createHook()); - Hook.use(HookTest(didBuild: deactivate2)); + Hook.use(HookTest(deactivate: deactivate2)); return Container(); }); @@ -135,8 +135,8 @@ void main() { ); verifyInOrder([ - deactivate2.call(), deactivate.call(), + deactivate2.call(), ]); }); From d54a1a72e821f01a126aa742a11c8988ac50bb91 Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Fri, 21 Feb 2020 16:47:05 +0000 Subject: [PATCH 109/384] added useFocusNode --- CHANGELOG.md | 14 +++++---- README.md | 20 +++++++------ example/.flutter-plugins-dependencies | 2 +- lib/src/focus.dart | 26 +++++++++++++++++ lib/src/hooks.dart | 1 + test/focus_test.dart | 41 +++++++++++++++++++++++++++ 6 files changed, 89 insertions(+), 15 deletions(-) create mode 100644 lib/src/focus.dart create mode 100644 test/focus_test.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index 2314d014..a3a1c10b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.8.0: + +Added `useFocusNode` + ## 0.7.0: - Added `useTextEditingController`, thanks to simolus3! @@ -16,16 +20,16 @@ ## 0.3.0: -- NEW: `usePrevious`, a hook that returns the previous argument it received. -- NEW: it is now impossible to call `inheritFromWidgetOfExactType` inside `initHook` of hooks. This forces authors to handle values updates. +- NEW: `usePrevious`, a hook that returns the previous argument is received. +- NEW: it is now impossible to call `inheritFromWidgetOfExactType` inside `initHook` of hooks. This forces authors to handle value updates. - FIX: use List instead of List for keys. This fixes `implicit-dynamic` rule mistakenly reporting errors. - NEW: Hooks are now visible on `HookElement` through `debugHooks` in development, for testing purposes. - NEW: If a widget throws on the first build or after a hot-reload, next rebuilds can still add/edit hooks until one `build` finishes entirely. -- NEW: new life-cycle availble on `HookState`: `didBuild`. +- NEW: new life-cycle available on `HookState`: `didBuild`. This life-cycle is called synchronously right after `build` method of `HookWidget` finished. - NEW: new `reassemble` life-cycle on `HookState`. It is equivalent to `State.ressemble` of statefulwidgets. - NEW: `useStream` and `useFuture` now have an optional `preserveState` flag. - This toggle how these hooks behaves when changing the stream/future: + This toggle how these hooks behave when changing the stream/future: If true (default) they keep the previous value, else they reset to initialState. ## 0.2.1: @@ -54,7 +58,7 @@ Widget build(BuildContext context) { - Introduced keys for hooks and applied them to hooks where it makes sense. - Added `useReducer` for complex state. It is similar to `useState` but is being managed by a `Reducer` and can only be changed by dispatching an action. -- fixes a bug where hot-reload without using hooks throwed an exception +- fixes a bug where hot-reload without using hooks thrown an exception ## 0.1.0: diff --git a/README.md b/README.md index b9d3235a..455279e3 100644 --- a/README.md +++ b/README.md @@ -6,11 +6,11 @@ A flutter implementation of React hooks: https://medium.com/@dan_abramov/making-sense-of-react-hooks-fdbde8803889 -Hooks are a new kind of object that manages a `Widget` life-cycles. They exist for one reason: increase the code sharing _between_ widgets and as a complete replacement for `StatefulWidget`. +Hooks are a new kind of object that manages a `Widget` life-cycles. They exist for one reason: increase the code-sharing _between_ widgets and as a complete replacement for `StatefulWidget`. ## Motivation -`StatefulWidget` suffer from a big problem: it is very difficult to reuse the logic of say `initState` or `dispose`. An obvious example is `AnimationController`: +`StatefulWidget` suffers from a big problem: it is very difficult to reuse the logic of say `initState` or `dispose`. An obvious example is `AnimationController`: ```dart class Example extends StatefulWidget { @@ -59,11 +59,11 @@ All widgets that desire to use an `AnimationController` will have to reimplement Dart mixins can partially solve this issue, but they suffer from other problems: - A given mixin can only be used once per class. -- Mixins and the class shares the same object. This means that if two mixins define a variable under the same name, the end result may vary between compilation fail to unknown behavior. +- Mixins and the class shares the same object. This means that if two mixins define a variable under the same name, the result may vary between compilation fail to unknown behavior. --- -This library propose a third solution: +This library proposes a third solution: ```dart class Example extends HookWidget { @@ -102,7 +102,7 @@ Widget build(BuildContext context) { } ``` -- Hooks are entirely independent of each other and from the widget. Which means they can easily be extracted into a package and published on [pub](https://pub.dartlang.org/) for others to use. +- Hooks are entirely independent of each other and from the widget. This means they can easily be extracted into a package and published on [pub](https://pub.dartlang.org/) for others to use. ## Principle @@ -131,7 +131,7 @@ For more explanation of how they are implemented, here's a great article about h ## Rules -Due to hooks being obtained from their index, there are some rules that must be respected: +Due to hooks being obtained from their index, some rules must be respected: ### DO call `use` unconditionally @@ -212,7 +212,8 @@ Hook.use(HookC()); ``` In this situation, `HookA` keeps its state but `HookC` gets a hard reset. -This happens because when a refactoring is done, all hooks _after_ the first line impacted are disposed. Since `HookC` was placed after `HookB`, is got disposed. +This happens because when a refactoring is done, all hooks _after_ the first line impacted are disposed of. +Since `HookC` was placed after `HookB`, is got disposed of. ## How to use @@ -281,11 +282,11 @@ class _TimeAliveState extends HookState> { Flutter_hooks comes with a list of reusable hooks already provided. -They are divided in different kinds: +They are divided into different kinds: ### Primitives -A set of low level hooks that interacts with the different life-cycles of a widget +A set of low-level hooks that interacts with the different life-cycles of a widget | name | description | | ----------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------- | @@ -333,3 +334,4 @@ A series of hooks with no particular theme. | [useReducer](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useReducer.html) | An alternative to `useState` for more complex states. | | [usePrevious](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/usePrevious.html) | Returns the previous argument called to [usePrevious]. | | [useTextEditingController](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useTextEditingController.html) | Create a `TextEditingController` | +| [useFocusNode](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useFocusNode.html) | Create a `FocusNode` | diff --git a/example/.flutter-plugins-dependencies b/example/.flutter-plugins-dependencies index 98ac26cf..134406b0 100644 --- a/example/.flutter-plugins-dependencies +++ b/example/.flutter-plugins-dependencies @@ -1 +1 @@ -{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"shared_preferences","path":"/Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/shared_preferences-0.4.3/","dependencies":[]}],"android":[{"name":"shared_preferences","path":"/Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/shared_preferences-0.4.3/","dependencies":[]}],"macos":[],"linux":[],"windows":[],"web":[]},"dependencyGraph":[{"name":"shared_preferences","dependencies":[]}],"date_created":"2020-01-27 17:55:50.387137","version":"1.14.5-pre.36"} \ No newline at end of file +{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"shared_preferences","path":"/Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/shared_preferences-0.4.3/","dependencies":[]}],"android":[{"name":"shared_preferences","path":"/Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/shared_preferences-0.4.3/","dependencies":[]}],"macos":[],"linux":[],"windows":[],"web":[]},"dependencyGraph":[{"name":"shared_preferences","dependencies":[]}],"date_created":"2020-01-27 18:13:12.175723","version":"1.14.5-pre.36"} \ No newline at end of file diff --git a/lib/src/focus.dart b/lib/src/focus.dart new file mode 100644 index 00000000..db64d1f0 --- /dev/null +++ b/lib/src/focus.dart @@ -0,0 +1,26 @@ +part of 'hooks.dart'; + +/// Creates and dispose of a [FocusNode]. +/// +/// See also: +/// - [FocusNode] +FocusNode useFocusNode() => Hook.use(const _FocusNodeHook()); + +class _FocusNodeHook extends Hook { + const _FocusNodeHook(); + + @override + _FocusNodeHookState createState() { + return _FocusNodeHookState(); + } +} + +class _FocusNodeHookState extends HookState { + final _focusNode = FocusNode(); + + @override + FocusNode build(BuildContext context) => _focusNode; + + @override + void dispose() => _focusNode?.dispose(); +} diff --git a/lib/src/hooks.dart b/lib/src/hooks.dart index 005ac468..bbec06e9 100644 --- a/lib/src/hooks.dart +++ b/lib/src/hooks.dart @@ -11,3 +11,4 @@ part 'listenable.dart'; part 'misc.dart'; part 'primitives.dart'; part 'text_controller.dart'; +part 'focus.dart'; diff --git a/test/focus_test.dart b/test/focus_test.dart new file mode 100644 index 00000000..67955652 --- /dev/null +++ b/test/focus_test.dart @@ -0,0 +1,41 @@ +import 'package:flutter/widgets.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; + +import 'mock.dart'; + +void main() { + testWidgets('creates a focus node', (tester) async { + FocusNode focusNode; + await tester.pumpWidget( + HookBuilder(builder: (_) { + focusNode = useFocusNode(); + return Container(); + }), + ); + + expect(focusNode, isA()); + // ignore: invalid_use_of_protected_member + expect(focusNode.hasListeners, isFalse); + + var previous = focusNode; + + await tester.pumpWidget( + HookBuilder(builder: (_) { + focusNode = useFocusNode(); + return Container(); + }), + ); + + expect(previous, focusNode); + // ignore: invalid_use_of_protected_member + expect(focusNode.hasListeners, isFalse); + + await tester.pumpWidget(Container()); + + expect( + // ignore: invalid_use_of_protected_member + () => focusNode.hasListeners, + throwsAssertionError, + ); + }); +} From 3c9b702e26aa02383c285f51a454660195856ac9 Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Fri, 21 Feb 2020 16:54:25 +0000 Subject: [PATCH 110/384] v0.8.0 --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index ab958eca..1a8ca5d9 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -3,7 +3,7 @@ description: A flutter implementation of React hooks. It adds a new kind of widg homepage: https://github.com/rrousselGit/flutter_hooks author: Remi Rousselet -version: 0.7.0 +version: 0.8.0 environment: sdk: ">=2.7.0 <3.0.0" From ba9fe730a3520f0d663ca856a02b8d112f8972ed Mon Sep 17 00:00:00 2001 From: Sahandevs Date: Mon, 24 Feb 2020 11:07:44 +0330 Subject: [PATCH 111/384] fix "should call other deactivates even if one fails" test --- test/hook_widget_test.dart | 71 +++++++++++++++++++++++++------------- 1 file changed, 47 insertions(+), 24 deletions(-) diff --git a/test/hook_widget_test.dart b/test/hook_widget_test.dart index af4b0a98..47fdc5fd 100644 --- a/test/hook_widget_test.dart +++ b/test/hook_widget_test.dart @@ -108,36 +108,59 @@ void main() { (tester) async { final deactivate2 = Func0(); final _key = GlobalKey(); + final onError = Func1(); + final oldOnError = FlutterError.onError; + FlutterError.onError = onError; + final errorBuilder = ErrorWidget.builder; + ErrorWidget.builder = Func1(); + when(ErrorWidget.builder(any)).thenReturn(Container()); + try { + when(deactivate2.call()).thenThrow(42); + when(builder.call(any)).thenAnswer((invocation) { + Hook.use(createHook()); + Hook.use(HookTest(deactivate: deactivate2)); + return Container(); + }); + await tester.pumpWidget(Column( + children: [ + Row( + children: [ + HookBuilder( + key: _key, + builder: builder.call, + ) + ], + ), + Container( + child: const SizedBox(), + ), + ], + )); - when(didBuild.call()).thenThrow(42); - when(builder.call(any)).thenAnswer((invocation) { - Hook.use(createHook()); - Hook.use(HookTest(deactivate: deactivate2)); - return Container(); - }); - - await expectPump( - () => tester.pumpWidget(HookBuilder( - key: _key, - builder: builder.call, - )), - throwsA(42), - ); - - await expectPump( - () => tester.pumpWidget(Container( + await tester.pumpWidget(Column( + children: [ + Row( + children: [], + ), + Container( child: HookBuilder( key: _key, builder: builder.call, ), - )), - throwsA(42), - ); + ), + ], + )); - verifyInOrder([ - deactivate.call(), - deactivate2.call(), - ]); + // reset the exception because after the test + // flutter tries to deactivate the widget and it causes + // and exception + when(deactivate2.call()).thenAnswer((_) {}); + verify(onError.call(any)).called((int x) => x > 1); + verify(deactivate.call()).called(1); + } finally { + FlutterError.onError = oldOnError; + ErrorWidget.builder = errorBuilder; + } }); testWidgets('should not allow using inheritedwidgets inside initHook', From ab7763206cfa6ebaea1cdf26779b4106cf3763ac Mon Sep 17 00:00:00 2001 From: creativecreatorormaybenot <19204050+creativecreatorormaybenot@users.noreply.github.com> Date: Mon, 30 Mar 2020 17:07:21 +0000 Subject: [PATCH 112/384] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 455279e3..cd811df4 100644 --- a/README.md +++ b/README.md @@ -86,7 +86,7 @@ But you're probably thinking: > Where did all the logic go? -That logic moved into `useAnimationController`, a function included directly in this library (see https://github.com/rrousselGit/flutter_hooks#existing-hooks). It is what we call a _Hook_. +That logic moved into `useAnimationController`, a function included directly in this library (see [Existing hooks](https://github.com/rrousselGit/flutter_hooks#existing-hooks)). It is what we call a _Hook_. Hooks are a new kind of objects with some specificities: From 17b4268a6dff05e6e239e44d55d48ea6531ffb02 Mon Sep 17 00:00:00 2001 From: creativecreatorormaybenot <19204050+creativecreatorormaybenot@users.noreply.github.com> Date: Mon, 30 Mar 2020 17:12:06 +0000 Subject: [PATCH 113/384] Update pubspec.yaml --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index 1a8ca5d9..17785f0a 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -3,7 +3,7 @@ description: A flutter implementation of React hooks. It adds a new kind of widg homepage: https://github.com/rrousselGit/flutter_hooks author: Remi Rousselet -version: 0.8.0 +version: 0.8.0+1 environment: sdk: ">=2.7.0 <3.0.0" From c7540fe6bdf0c112bd1f4ccba778f72dac7b8f8e Mon Sep 17 00:00:00 2001 From: creativecreatorormaybenot <19204050+creativecreatorormaybenot@users.noreply.github.com> Date: Mon, 30 Mar 2020 17:12:43 +0000 Subject: [PATCH 114/384] Update CHANGELOG.md --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a3a1c10b..c9d0e880 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.8.0+1 + +- Fixed link to "Existing hooks" in `README.md`. + ## 0.8.0: Added `useFocusNode` From 972cf34ca3e308bda95fff46b72a1a363923ff14 Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Sat, 11 Apr 2020 11:04:45 +0100 Subject: [PATCH 115/384] v0.9.0 --- CHANGELOG.md | 4 ++ example/.flutter-plugins-dependencies | 2 +- example/pubspec.lock | 44 ++++-------------- pubspec.lock | 67 ++++----------------------- pubspec.yaml | 2 +- 5 files changed, 23 insertions(+), 96 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c9d0e880..e7064e3c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.9.0 + +- Added a `deactivate` life-cycle to `HookState` + ## 0.8.0+1 - Fixed link to "Existing hooks" in `README.md`. diff --git a/example/.flutter-plugins-dependencies b/example/.flutter-plugins-dependencies index 134406b0..04a64a12 100644 --- a/example/.flutter-plugins-dependencies +++ b/example/.flutter-plugins-dependencies @@ -1 +1 @@ -{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"shared_preferences","path":"/Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/shared_preferences-0.4.3/","dependencies":[]}],"android":[{"name":"shared_preferences","path":"/Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/shared_preferences-0.4.3/","dependencies":[]}],"macos":[],"linux":[],"windows":[],"web":[]},"dependencyGraph":[{"name":"shared_preferences","dependencies":[]}],"date_created":"2020-01-27 18:13:12.175723","version":"1.14.5-pre.36"} \ No newline at end of file +{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"shared_preferences","path":"/Users/remiguest/.pub-cache/hosted/pub.dartlang.org/shared_preferences-0.4.3/","dependencies":[]}],"android":[{"name":"shared_preferences","path":"/Users/remiguest/.pub-cache/hosted/pub.dartlang.org/shared_preferences-0.4.3/","dependencies":[]}],"macos":[],"linux":[],"windows":[],"web":[]},"dependencyGraph":[{"name":"shared_preferences","dependencies":[]}],"date_created":"2020-04-11 11:04:13.397414","version":"1.18.0-5.0.pre.57"} \ No newline at end of file diff --git a/example/pubspec.lock b/example/pubspec.lock index fddefee8..b4385253 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -15,13 +15,6 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.2.1" - archive: - dependency: transitive - description: - name: archive - url: "https://pub.dartlang.org" - source: hosted - version: "2.0.11" args: dependency: transitive description: @@ -35,14 +28,14 @@ packages: name: async url: "https://pub.dartlang.org" source: hosted - version: "2.4.0" + version: "2.4.1" boolean_selector: dependency: transitive description: name: boolean_selector url: "https://pub.dartlang.org" source: hosted - version: "1.0.5" + version: "2.0.0" build: dependency: transitive description: @@ -112,7 +105,7 @@ packages: name: charcode url: "https://pub.dartlang.org" source: hosted - version: "1.1.2" + version: "1.1.3" checked_yaml: dependency: transitive description: @@ -133,7 +126,7 @@ packages: name: collection url: "https://pub.dartlang.org" source: hosted - version: "1.14.11" + version: "1.14.12" convert: dependency: transitive description: @@ -180,7 +173,7 @@ packages: path: ".." relative: true source: path - version: "0.7.0" + version: "0.9.0" flutter_test: dependency: "direct dev" description: flutter @@ -235,13 +228,6 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "3.1.3" - image: - dependency: transitive - description: - name: image - url: "https://pub.dartlang.org" - source: hosted - version: "2.1.4" io: dependency: transitive description: @@ -340,13 +326,6 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.8.0+1" - petitparser: - dependency: transitive - description: - name: petitparser - url: "https://pub.dartlang.org" - source: hosted - version: "2.4.0" pool: dependency: transitive description: @@ -381,7 +360,7 @@ packages: name: quiver url: "https://pub.dartlang.org" source: hosted - version: "2.0.5" + version: "2.1.3" shared_preferences: dependency: "direct main" description: @@ -421,7 +400,7 @@ packages: name: source_span url: "https://pub.dartlang.org" source: hosted - version: "1.5.5" + version: "1.7.0" stack_trace: dependency: transitive description: @@ -463,7 +442,7 @@ packages: name: test_api url: "https://pub.dartlang.org" source: hosted - version: "0.2.11" + version: "0.2.15" timing: dependency: transitive description: @@ -499,13 +478,6 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.1.0" - xml: - dependency: transitive - description: - name: xml - url: "https://pub.dartlang.org" - source: hosted - version: "3.5.0" yaml: dependency: transitive description: diff --git a/pubspec.lock b/pubspec.lock index d1b672d9..c1c5dd3b 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1,62 +1,34 @@ # Generated by pub # See https://dart.dev/tools/pub/glossary#lockfile packages: - archive: - dependency: transitive - description: - name: archive - url: "https://pub.dartlang.org" - source: hosted - version: "2.0.11" - args: - dependency: transitive - description: - name: args - url: "https://pub.dartlang.org" - source: hosted - version: "1.5.2" async: dependency: transitive description: name: async url: "https://pub.dartlang.org" source: hosted - version: "2.4.0" + version: "2.4.1" boolean_selector: dependency: transitive description: name: boolean_selector url: "https://pub.dartlang.org" source: hosted - version: "1.0.5" + version: "2.0.0" charcode: dependency: transitive description: name: charcode url: "https://pub.dartlang.org" source: hosted - version: "1.1.2" + version: "1.1.3" collection: dependency: transitive description: name: collection url: "https://pub.dartlang.org" source: hosted - version: "1.14.11" - convert: - dependency: transitive - description: - name: convert - url: "https://pub.dartlang.org" - source: hosted - version: "2.1.1" - crypto: - dependency: transitive - description: - name: crypto - url: "https://pub.dartlang.org" - source: hosted - version: "2.1.3" + version: "1.14.12" flutter: dependency: "direct main" description: flutter @@ -67,13 +39,6 @@ packages: description: flutter source: sdk version: "0.0.0" - image: - dependency: transitive - description: - name: image - url: "https://pub.dartlang.org" - source: hosted - version: "2.1.4" matcher: dependency: transitive description: @@ -94,7 +59,7 @@ packages: name: mockito url: "https://pub.dartlang.org" source: hosted - version: "4.0.0" + version: "4.1.1" path: dependency: transitive description: @@ -108,21 +73,14 @@ packages: name: pedantic url: "https://pub.dartlang.org" source: hosted - version: "1.8.0+1" - petitparser: - dependency: transitive - description: - name: petitparser - url: "https://pub.dartlang.org" - source: hosted - version: "2.4.0" + version: "1.9.0" quiver: dependency: transitive description: name: quiver url: "https://pub.dartlang.org" source: hosted - version: "2.0.5" + version: "2.1.3" sky_engine: dependency: transitive description: flutter @@ -134,7 +92,7 @@ packages: name: source_span url: "https://pub.dartlang.org" source: hosted - version: "1.5.5" + version: "1.7.0" stack_trace: dependency: transitive description: @@ -169,7 +127,7 @@ packages: name: test_api url: "https://pub.dartlang.org" source: hosted - version: "0.2.11" + version: "0.2.15" typed_data: dependency: transitive description: @@ -184,12 +142,5 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.0.8" - xml: - dependency: transitive - description: - name: xml - url: "https://pub.dartlang.org" - source: hosted - version: "3.5.0" sdks: dart: ">=2.7.0 <3.0.0" diff --git a/pubspec.yaml b/pubspec.yaml index 17785f0a..819fa585 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -3,7 +3,7 @@ description: A flutter implementation of React hooks. It adds a new kind of widg homepage: https://github.com/rrousselGit/flutter_hooks author: Remi Rousselet -version: 0.8.0+1 +version: 0.9.0 environment: sdk: ">=2.7.0 <3.0.0" From ab9f4182fa8c69944e194e48eca0fe7fc9ffccd2 Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Sun, 7 Jun 2020 04:03:27 +0100 Subject: [PATCH 116/384] upddate lints --- all_lint_rules.yaml | 167 +++++++++++++++++++++ analysis_options.yaml | 130 +++++++--------- example/.flutter-plugins-dependencies | 2 +- example/analysis_options.yaml | 2 + example/lib/custom_hook_function.dart | 4 +- example/lib/main.dart | 13 +- example/lib/star_wars/models.dart | 19 ++- example/lib/star_wars/planet_screen.dart | 27 ++-- example/lib/star_wars/redux.dart | 26 ++-- example/lib/star_wars/star_wars_api.dart | 8 +- example/lib/use_effect.dart | 8 +- example/lib/use_state.dart | 2 +- example/pubspec.lock | 20 ++- lib/src/animation.dart | 39 ++--- lib/src/async.dart | 24 +-- lib/src/framework.dart | 134 ++++++++++++----- lib/src/hooks.dart | 3 +- lib/src/listenable.dart | 9 +- lib/src/misc.dart | 27 ++-- lib/src/primitives.dart | 34 +++-- lib/src/text_controller.dart | 14 +- pubspec.lock | 32 ++-- pubspec.yaml | 3 +- test/focus_test.dart | 4 +- test/hook_builder_test.dart | 3 +- test/hook_widget_test.dart | 144 +++++++++--------- test/memoized_test.dart | 4 +- test/mock.dart | 25 +-- test/use_animation_controller_test.dart | 6 +- test/use_animation_test.dart | 14 +- test/use_effect_test.dart | 22 +-- test/use_future_test.dart | 21 +-- test/use_listenable_test.dart | 14 +- test/use_reassemble_test.dart | 2 +- test/use_reducer_test.dart | 28 ++-- test/use_stream_controller_test.dart | 8 +- test/use_stream_test.dart | 19 +-- test/use_text_editing_controller_test.dart | 8 +- test/use_value_changed_test.dart | 2 +- test/use_value_listenable_test.dart | 14 +- test/use_value_notifier_test.dart | 2 +- 41 files changed, 668 insertions(+), 419 deletions(-) create mode 100644 all_lint_rules.yaml diff --git a/all_lint_rules.yaml b/all_lint_rules.yaml new file mode 100644 index 00000000..238664b5 --- /dev/null +++ b/all_lint_rules.yaml @@ -0,0 +1,167 @@ +linter: + rules: + - always_declare_return_types + - always_put_control_body_on_new_line + - always_put_required_named_parameters_first + - always_require_non_null_named_parameters + - always_specify_types + - annotate_overrides + - avoid_annotating_with_dynamic + - avoid_as + - avoid_bool_literals_in_conditional_expressions + - avoid_catches_without_on_clauses + - avoid_catching_errors + - avoid_classes_with_only_static_members + - avoid_double_and_int_checks + - avoid_empty_else + - avoid_equals_and_hash_code_on_mutable_classes + - avoid_escaping_inner_quotes + - avoid_field_initializers_in_const_classes + - avoid_function_literals_in_foreach_calls + - avoid_implementing_value_types + - avoid_init_to_null + - avoid_js_rounded_ints + - avoid_null_checks_in_equality_operators + - avoid_positional_boolean_parameters + - avoid_print + - avoid_private_typedef_functions + - avoid_redundant_argument_values + - avoid_relative_lib_imports + - avoid_renaming_method_parameters + - avoid_return_types_on_setters + - avoid_returning_null + - avoid_returning_null_for_future + - avoid_returning_null_for_void + - avoid_returning_this + - avoid_setters_without_getters + - avoid_shadowing_type_parameters + - avoid_single_cascade_in_expression_statements + - avoid_slow_async_io + - avoid_types_as_parameter_names + - avoid_types_on_closure_parameters + - avoid_unnecessary_containers + - avoid_unused_constructor_parameters + - avoid_void_async + - avoid_web_libraries_in_flutter + - await_only_futures + - camel_case_extensions + - camel_case_types + - cancel_subscriptions + - cascade_invocations + - close_sinks + - comment_references + - constant_identifier_names + - control_flow_in_finally + - curly_braces_in_flow_control_structures + - diagnostic_describe_all_properties + - directives_ordering + - empty_catches + - empty_constructor_bodies + - empty_statements + - file_names + - flutter_style_todos + - hash_and_equals + - implementation_imports + - invariant_booleans + - iterable_contains_unrelated_type + - join_return_with_assignment + - leading_newlines_in_multiline_strings + - library_names + - library_prefixes + - lines_longer_than_80_chars + - list_remove_unrelated_type + - literal_only_boolean_expressions + - missing_whitespace_between_adjacent_strings + - no_adjacent_strings_in_list + - no_duplicate_case_values + - no_logic_in_create_state + - no_runtimeType_toString + - non_constant_identifier_names + - null_closures + - omit_local_variable_types + - one_member_abstracts + - only_throw_errors + - overridden_fields + - package_api_docs + - package_names + - package_prefixed_library_names + - parameter_assignments + - prefer_adjacent_string_concatenation + - prefer_asserts_in_initializer_lists + - prefer_asserts_with_message + - prefer_collection_literals + - prefer_conditional_assignment + - prefer_const_constructors + - prefer_const_constructors_in_immutables + - prefer_const_declarations + - prefer_const_literals_to_create_immutables + - prefer_constructors_over_static_methods + - prefer_contains + - prefer_double_quotes + - prefer_equal_for_default_values + - prefer_expression_function_bodies + - prefer_final_fields + - prefer_final_in_for_each + - prefer_final_locals + - prefer_for_elements_to_map_fromIterable + - prefer_foreach + - prefer_function_declarations_over_variables + - prefer_generic_function_type_aliases + - prefer_if_elements_to_conditional_expressions + - prefer_if_null_operators + - prefer_initializing_formals + - prefer_inlined_adds + - prefer_int_literals + - prefer_interpolation_to_compose_strings + - prefer_is_empty + - prefer_is_not_empty + - prefer_is_not_operator + - prefer_iterable_whereType + - prefer_mixin + - prefer_null_aware_operators + - prefer_relative_imports + - prefer_single_quotes + - prefer_spread_collections + - prefer_typing_uninitialized_variables + - prefer_void_to_null + - provide_deprecation_message + - public_member_api_docs + - recursive_getters + - slash_for_doc_comments + - sort_child_properties_last + - sort_constructors_first + - sort_pub_dependencies + - sort_unnamed_constructors_first + - test_types_in_equals + - throw_in_finally + - type_annotate_public_apis + - type_init_formals + - unawaited_futures + - unnecessary_await_in_return + - unnecessary_brace_in_string_interps + - unnecessary_const + - unnecessary_final + - unnecessary_getters_setters + - unnecessary_lambdas + - unnecessary_new + - unnecessary_null_aware_assignments + - unnecessary_null_in_if_null_operators + - unnecessary_overrides + - unnecessary_parenthesis + - unnecessary_raw_strings + - unnecessary_statements + - unnecessary_string_escapes + - unnecessary_string_interpolations + - unnecessary_this + - unrelated_type_equality_checks + - unsafe_html + - use_full_hex_values_for_flutter_colors + - use_function_type_syntax_for_parameters + - use_key_in_widget_constructors + - use_raw_strings + - use_rethrow_when_possible + - use_setters_to_change_properties + - use_string_buffers + - use_to_and_as_if_applicable + - valid_regexps + - void_checks \ No newline at end of file diff --git a/analysis_options.yaml b/analysis_options.yaml index cd7a9506..dc5b2590 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -1,80 +1,64 @@ -include: package:pedantic/analysis_options.yaml +include: all_lint_rules.yaml analyzer: + exclude: + - "**/*.g.dart" + - "**/*.freezed.dart" strong-mode: implicit-casts: false implicit-dynamic: false errors: - todo: error - include_file_not_found: ignore + # Otherwise cause the import of all_lint_rules to warn because of some rules conflicts. + # We explicitly enabled even conflicting rules and are fixing the conflict + # in this file + included_file_warning: ignore + + # Causes false positives (https://github.com/dart-lang/sdk/issues/41571 + top_level_function_literal_block: ignore + linter: rules: - - public_member_api_docs - - annotate_overrides - - avoid_empty_else - - avoid_function_literals_in_foreach_calls - - avoid_init_to_null - - avoid_null_checks_in_equality_operators - - avoid_relative_lib_imports - - avoid_renaming_method_parameters - - avoid_return_types_on_setters - - avoid_returning_null - - avoid_types_as_parameter_names - - avoid_unused_constructor_parameters - - await_only_futures - - camel_case_types - - cancel_subscriptions - - cascade_invocations - - comment_references - - constant_identifier_names - - control_flow_in_finally - - directives_ordering - - empty_catches - - empty_constructor_bodies - - empty_statements - - hash_and_equals - - implementation_imports - - invariant_booleans - - iterable_contains_unrelated_type - - library_names - - library_prefixes - - list_remove_unrelated_type - - no_adjacent_strings_in_list - - no_duplicate_case_values - - non_constant_identifier_names - - null_closures - - omit_local_variable_types - - only_throw_errors - - overridden_fields - - package_api_docs - - package_names - - package_prefixed_library_names - - prefer_adjacent_string_concatenation - - prefer_collection_literals - - prefer_conditional_assignment - - prefer_const_constructors - - prefer_contains - - prefer_equal_for_default_values - - prefer_final_fields - - prefer_initializing_formals - - prefer_interpolation_to_compose_strings - - prefer_is_empty - - prefer_is_not_empty - - prefer_single_quotes - - prefer_typing_uninitialized_variables - - recursive_getters - - slash_for_doc_comments - - test_types_in_equals - - throw_in_finally - - type_init_formals - - unawaited_futures - - unnecessary_brace_in_string_interps - - unnecessary_const - - unnecessary_getters_setters - - unnecessary_lambdas - - unnecessary_new - - unnecessary_null_aware_assignments - - unnecessary_statements - - unnecessary_this - - unrelated_type_equality_checks - - use_rethrow_when_possible - - valid_regexps + # Personal preference. I don't find it more readable + cascade_invocations: false + + # Conflicts with `prefer_single_quotes` + # Single quotes are easier to type and don't compromise on readability. + prefer_double_quotes: false + + # Conflicts with `omit_local_variable_types` and other rules. + # As per Dart guidelines, we want to avoid unnecessary types to make the code + # more readable. + # See https://dart.dev/guides/language/effective-dart/design#avoid-type-annotating-initialized-local-variables + always_specify_types: false + + # Incompatible with `prefer_final_locals` + # Having immutable local variables makes larger functions more predictible + # so we will use `prefer_final_locals` instead. + unnecessary_final: false + + # Not quite suitable for Flutter, which may have a `build` method with a single + # return, but that return is still complex enough that a "body" is worth it. + prefer_expression_function_bodies: false + + # Conflicts with the convention used by flutter, which puts `Key key` + # and `@required Widget child` last. + always_put_required_named_parameters_first: false + + # `as` is not that bad (especially with the upcoming non-nullable types). + # Explicit exceptions is better than implicit exceptions. + avoid_as: false + + # This project doesn't use Flutter-style todos + flutter_style_todos: false + + # There are situations where we voluntarily want to catch everything, + # especially as a library. + avoid_catches_without_on_clauses: false + + # Boring as it sometimes force a line of 81 characters to be split in two. + # As long as we try to respect that 80 characters limit, going slightly + # above is fine. + lines_longer_than_80_chars: false + + # Conflicts with disabling `implicit-dynamic` + avoid_annotating_with_dynamic: false + diff --git a/example/.flutter-plugins-dependencies b/example/.flutter-plugins-dependencies index 04a64a12..80366350 100644 --- a/example/.flutter-plugins-dependencies +++ b/example/.flutter-plugins-dependencies @@ -1 +1 @@ -{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"shared_preferences","path":"/Users/remiguest/.pub-cache/hosted/pub.dartlang.org/shared_preferences-0.4.3/","dependencies":[]}],"android":[{"name":"shared_preferences","path":"/Users/remiguest/.pub-cache/hosted/pub.dartlang.org/shared_preferences-0.4.3/","dependencies":[]}],"macos":[],"linux":[],"windows":[],"web":[]},"dependencyGraph":[{"name":"shared_preferences","dependencies":[]}],"date_created":"2020-04-11 11:04:13.397414","version":"1.18.0-5.0.pre.57"} \ No newline at end of file +{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"shared_preferences","path":"/Users/remiguest/.pub-cache/hosted/pub.dartlang.org/shared_preferences-0.4.3/","dependencies":[]}],"android":[{"name":"shared_preferences","path":"/Users/remiguest/.pub-cache/hosted/pub.dartlang.org/shared_preferences-0.4.3/","dependencies":[]}],"macos":[],"linux":[],"windows":[],"web":[]},"dependencyGraph":[{"name":"shared_preferences","dependencies":[]}],"date_created":"2020-06-07 03:18:10.917665","version":"1.19.0-4.0.pre.73"} \ No newline at end of file diff --git a/example/analysis_options.yaml b/example/analysis_options.yaml index b6d5666f..f52cc3f8 100644 --- a/example/analysis_options.yaml +++ b/example/analysis_options.yaml @@ -9,3 +9,5 @@ analyzer: linter: rules: public_member_api_docs: false + avoid_print: false + use_key_in_widget_constructors: false diff --git a/example/lib/custom_hook_function.dart b/example/lib/custom_hook_function.dart index cbdfb7d0..168de53c 100644 --- a/example/lib/custom_hook_function.dart +++ b/example/lib/custom_hook_function.dart @@ -21,11 +21,11 @@ class CustomHookFunctionExample extends HookWidget { child: Text('Button tapped ${counter.value} times'), ), floatingActionButton: FloatingActionButton( - child: const Icon(Icons.add), // When the button is pressed, update the value of the counter! This // will trigger a rebuild as well as printing the latest value to the // console! onPressed: () => counter.value++, + child: const Icon(Icons.add), ), ); } @@ -39,7 +39,7 @@ ValueNotifier useLoggedState([T initialData]) { final result = useState(initialData); // Next, call the useValueChanged hook to print the state whenever it changes - useValueChanged(result.value, (T _, T __) { + useValueChanged(result.value, (_, __) { print(result.value); }); diff --git a/example/lib/main.dart b/example/lib/main.dart index 96f2385c..9570ada7 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -1,9 +1,10 @@ // ignore_for_file: omit_local_variable_types import 'package:flutter/material.dart'; -import 'package:flutter_hooks_gallery/star_wars/planet_screen.dart'; -import 'package:flutter_hooks_gallery/use_effect.dart'; -import 'package:flutter_hooks_gallery/use_state.dart'; -import 'package:flutter_hooks_gallery/use_stream.dart'; + +import 'star_wars/planet_screen.dart'; +import 'use_effect.dart'; +import 'use_state.dart'; +import 'use_stream.dart'; void main() => runApp(HooksGalleryApp()); @@ -43,11 +44,11 @@ class HooksGalleryApp extends StatelessWidget { } class _GalleryItem extends StatelessWidget { + const _GalleryItem({this.title, this.builder}); + final String title; final WidgetBuilder builder; - const _GalleryItem({this.title, this.builder}); - @override Widget build(BuildContext context) { return ListTile( diff --git a/example/lib/star_wars/models.dart b/example/lib/star_wars/models.dart index 6cb32683..fe2370ff 100644 --- a/example/lib/star_wars/models.dart +++ b/example/lib/star_wars/models.dart @@ -4,6 +4,7 @@ import 'package:built_collection/built_collection.dart'; import 'package:built_value/built_value.dart'; import 'package:built_value/serializer.dart'; import 'package:built_value/standard_json_plugin.dart'; +import 'package:meta/meta.dart'; part 'models.g.dart'; @@ -15,13 +16,14 @@ part 'models.g.dart'; final Serializers serializers = (_$serializers.toBuilder()..addPlugin(StandardJsonPlugin())).build(); -/// equals one page +@immutable abstract class PlanetPageModel implements Built { - PlanetPageModel._(); + factory PlanetPageModel([ + void Function(PlanetPageModelBuilder) updates, + ]) = _$PlanetPageModel; - factory PlanetPageModel([void Function(PlanetPageModelBuilder) updates]) = - _$PlanetPageModel; + const PlanetPageModel._(); static Serializer get serializer => _$planetPageModelSerializer; @@ -35,12 +37,13 @@ abstract class PlanetPageModel BuiltList get results; } -/// equals one planet +@immutable abstract class PlanetModel implements Built { - PlanetModel._(); + factory PlanetModel([ + void Function(PlanetModelBuilder) updates, + ]) = _$PlanetModel; - factory PlanetModel([void Function(PlanetModelBuilder) updates]) = - _$PlanetModel; + const PlanetModel._(); static Serializer get serializer => _$planetModelSerializer; diff --git a/example/lib/star_wars/planet_screen.dart b/example/lib/star_wars/planet_screen.dart index 276dd36a..959886a5 100644 --- a/example/lib/star_wars/planet_screen.dart +++ b/example/lib/star_wars/planet_screen.dart @@ -1,9 +1,10 @@ import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:flutter_hooks_gallery/star_wars/redux.dart'; -import 'package:flutter_hooks_gallery/star_wars/star_wars_api.dart'; import 'package:provider/provider.dart'; +import 'redux.dart'; +import 'star_wars_api.dart'; + /// This handler will take care of async api interactions /// and updating the store afterwards. class _PlanetHandler { @@ -89,10 +90,10 @@ class _PlanetScreenBody extends HookWidget { } class _Error extends StatelessWidget { - final String errorMsg; - const _Error({Key key, this.errorMsg}) : super(key: key); + final String errorMsg; + @override Widget build(BuildContext context) { return Column( @@ -101,11 +102,13 @@ class _Error extends StatelessWidget { if (errorMsg != null) Text(errorMsg), RaisedButton( color: Colors.redAccent, - child: const Text('Try again'), onPressed: () async { - await Provider.of<_PlanetHandler>(context, listen: false) - .fetchAndDispatch(); + await Provider.of<_PlanetHandler>( + context, + listen: false, + ).fetchAndDispatch(); }, + child: const Text('Try again'), ), ], ); @@ -113,7 +116,8 @@ class _Error extends StatelessWidget { } class _LoadPageButton extends HookWidget { - _LoadPageButton({this.next = true}) : assert(next != null); + const _LoadPageButton({this.next = true}) + : assert(next != null, 'next cannot be null'); final bool next; @@ -121,12 +125,12 @@ class _LoadPageButton extends HookWidget { Widget build(BuildContext context) { final state = Provider.of(context); return RaisedButton( - child: next ? const Text('Next Page') : const Text('Prev Page'), onPressed: () async { final url = next ? state.planetPage.next : state.planetPage.previous; await Provider.of<_PlanetHandler>(context, listen: false) .fetchAndDispatch(url); }, + child: next ? const Text('Next Page') : const Text('Prev Page'), ); } } @@ -165,8 +169,9 @@ class _PlanetListHeader extends StatelessWidget { return Row( mainAxisAlignment: buttonAlignment, children: [ - if (state.planetPage.previous != null) _LoadPageButton(next: false), - if (state.planetPage.next != null) _LoadPageButton(next: true) + if (state.planetPage.previous != null) + const _LoadPageButton(next: false), + if (state.planetPage.next != null) const _LoadPageButton() ], ); } diff --git a/example/lib/star_wars/redux.dart b/example/lib/star_wars/redux.dart index e1706def..110ef94a 100644 --- a/example/lib/star_wars/redux.dart +++ b/example/lib/star_wars/redux.dart @@ -1,5 +1,7 @@ import 'package:built_value/built_value.dart'; -import 'package:flutter_hooks_gallery/star_wars/models.dart'; +import 'package:meta/meta.dart'; + +import 'models.dart'; part 'redux.g.dart'; @@ -11,44 +13,36 @@ class FetchPlanetPageActionStart extends ReduxAction {} /// Action that updates state to show that we are loading planets class FetchPlanetPageActionError extends ReduxAction { + FetchPlanetPageActionError(this.errorMsg); + /// Message that should be displayed in the UI final String errorMsg; - - /// constructor - FetchPlanetPageActionError(this.errorMsg); } /// Action to set the planet page class FetchPlanetPageActionSuccess extends ReduxAction { - /// payload - final PlanetPageModel page; - - /// constructor FetchPlanetPageActionSuccess(this.page); + + final PlanetPageModel page; } -/// state of the redux store +@immutable abstract class AppState implements Built { - AppState._(); - - /// default factory factory AppState([void Function(AppStateBuilder) updates]) => _$AppState((u) => u ..isFetchingPlanets = false ..update(updates)); - /// are we currently loading planets + const AppState._(); + bool get isFetchingPlanets; - /// will be set if loading planets failed. This is an error message @nullable String get errorFetchingPlanets; - /// current planet page PlanetPageModel get planetPage; } -/// reducer that is used by useReducer to create the redux store AppState reducer(S state, A action) { final b = state.toBuilder(); if (action is FetchPlanetPageActionStart) { diff --git a/example/lib/star_wars/star_wars_api.dart b/example/lib/star_wars/star_wars_api.dart index b81a802d..a53c60b7 100644 --- a/example/lib/star_wars/star_wars_api.dart +++ b/example/lib/star_wars/star_wars_api.dart @@ -1,15 +1,17 @@ import 'dart:convert'; -import 'package:flutter_hooks_gallery/star_wars/models.dart'; import 'package:http/http.dart' as http; +import 'models.dart'; + /// Api wrapper to retrieve Star Wars related data class StarWarsApi { /// load and return one page of planets - Future getPlanets(String page) async { + Future getPlanets([String page]) async { page ??= 'https://swapi.co/api/planets'; + final response = await http.get(page); - dynamic json = jsonDecode(utf8.decode(response.bodyBytes)); + final dynamic json = jsonDecode(utf8.decode(response.bodyBytes)); return serializers.deserializeWith(PlanetPageModel.serializer, json); } diff --git a/example/lib/use_effect.dart b/example/lib/use_effect.dart index 194d08d8..d15c23c0 100644 --- a/example/lib/use_effect.dart +++ b/example/lib/use_effect.dart @@ -15,7 +15,9 @@ class CustomHookExample extends HookWidget { // To update the stored value, `add` data to the StreamController. To get // the latest value from the StreamController, listen to the Stream with // the useStream hook. - StreamController countController = _useLocalStorageInt('counter'); + // ignore: close_sinks + final StreamController countController = + _useLocalStorageInt('counter'); return Scaffold( appBar: AppBar( @@ -27,7 +29,7 @@ class CustomHookExample extends HookWidget { // new value child: HookBuilder( builder: (context) { - AsyncSnapshot count = useStream(countController.stream); + final AsyncSnapshot count = useStream(countController.stream); return !count.hasData ? const CircularProgressIndicator() @@ -76,7 +78,7 @@ StreamController _useLocalStorageInt( useEffect( () { SharedPreferences.getInstance().then((prefs) async { - int valueFromStorage = prefs.getInt(key); + final int valueFromStorage = prefs.getInt(key); controller.add(valueFromStorage ?? defaultValue); }).catchError(controller.addError); return null; diff --git a/example/lib/use_state.dart b/example/lib/use_state.dart index ea62e6df..770fee00 100644 --- a/example/lib/use_state.dart +++ b/example/lib/use_state.dart @@ -22,11 +22,11 @@ class UseStateExample extends HookWidget { child: Text('Button tapped ${counter.value} times'), ), floatingActionButton: FloatingActionButton( - child: const Icon(Icons.add), // When the button is pressed, update the value of the counter! This // will trigger a rebuild, displaying the latest value in the Text // Widget above! onPressed: () => counter.value++, + child: const Icon(Icons.add), ), ); } diff --git a/example/pubspec.lock b/example/pubspec.lock index b4385253..b7f532ec 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -113,6 +113,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.0.2" + clock: + dependency: transitive + description: + name: clock + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.1" code_builder: dependency: transitive description: @@ -155,6 +162,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.3.3" + fake_async: + dependency: transitive + description: + name: fake_async + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.0" fixnum: dependency: transitive description: @@ -173,7 +187,7 @@ packages: path: ".." relative: true source: path - version: "0.9.0" + version: "0.10.0-dev" flutter_test: dependency: "direct dev" description: flutter @@ -318,7 +332,7 @@ packages: name: path url: "https://pub.dartlang.org" source: hosted - version: "1.6.4" + version: "1.7.0" pedantic: dependency: transitive description: @@ -442,7 +456,7 @@ packages: name: test_api url: "https://pub.dartlang.org" source: hosted - version: "0.2.15" + version: "0.2.16" timing: dependency: transitive description: diff --git a/lib/src/animation.dart b/lib/src/animation.dart index 3c32126a..8be494b6 100644 --- a/lib/src/animation.dart +++ b/lib/src/animation.dart @@ -46,14 +46,6 @@ AnimationController useAnimationController({ } class _AnimationControllerHook extends Hook { - final Duration duration; - final String debugLabel; - final double initialValue; - final double lowerBound; - final double upperBound; - final TickerProvider vsync; - final AnimationBehavior animationBehavior; - const _AnimationControllerHook({ this.duration, this.debugLabel, @@ -65,6 +57,14 @@ class _AnimationControllerHook extends Hook { List keys, }) : super(keys: keys); + final Duration duration; + final String debugLabel; + final double initialValue; + final double lowerBound; + final double upperBound; + final TickerProvider vsync; + final AnimationBehavior animationBehavior; + @override _AnimationControllerHookState createState() => _AnimationControllerHookState(); @@ -93,7 +93,7 @@ Switching between controller and uncontrolled vsync is not allowed. AnimationController build(BuildContext context) { final vsync = hook.vsync ?? useSingleTickerProvider(keys: hook.keys); - _animationController ??= AnimationController( + return _animationController ??= AnimationController( vsync: vsync, duration: hook.duration, debugLabel: hook.debugLabel, @@ -102,8 +102,6 @@ Switching between controller and uncontrolled vsync is not allowed. animationBehavior: hook.animationBehavior, value: hook.initialValue, ); - - return _animationController; } @override @@ -139,33 +137,38 @@ class _TickerProviderHookState @override Ticker createTicker(TickerCallback onTick) { assert(() { - if (_ticker == null) return true; + if (_ticker == null) { + return true; + } throw FlutterError( '${context.widget.runtimeType} attempted to use a useSingleTickerProvider multiple times.\n' 'A SingleTickerProviderStateMixin can only be used as a TickerProvider once. If a ' 'TickerProvider is used for multiple AnimationController objects, or if it is passed to other ' 'objects and those objects might use it more than one time in total, then instead of ' 'using useSingleTickerProvider, use a regular useTickerProvider.'); - }()); - _ticker = Ticker(onTick, debugLabel: 'created by $context'); - return _ticker; + }(), ''); + return _ticker = Ticker(onTick, debugLabel: 'created by $context'); } @override void dispose() { assert(() { - if (_ticker == null || !_ticker.isActive) return true; + if (_ticker == null || !_ticker.isActive) { + return true; + } throw FlutterError( 'useSingleTickerProvider created a Ticker, but at the time ' 'dispose() was called on the Hook, that Ticker was still active. Tickers used ' ' by AnimationControllers should be disposed by calling dispose() on ' ' the AnimationController itself. Otherwise, the ticker will leak.\n'); - }()); + }(), ''); } @override TickerProvider build(BuildContext context) { - if (_ticker != null) _ticker.muted = !TickerMode.of(context); + if (_ticker != null) { + _ticker.muted = !TickerMode.of(context); + } return this; } } diff --git a/lib/src/async.dart b/lib/src/async.dart index fb1a1a53..ef3bc4bd 100644 --- a/lib/src/async.dart +++ b/lib/src/async.dart @@ -15,12 +15,12 @@ AsyncSnapshot useFuture(Future future, } class _FutureHook extends Hook> { + const _FutureHook(this.future, {this.initialData, this.preserveState = true}); + final Future future; final bool preserveState; final T initialData; - const _FutureHook(this.future, {this.initialData, this.preserveState = true}); - @override _FutureStateHook createState() => _FutureStateHook(); } @@ -66,13 +66,13 @@ class _FutureStateHook extends HookState, _FutureHook> { if (hook.future != null) { final callbackIdentity = Object(); _activeCallbackIdentity = callbackIdentity; - hook.future.then((T data) { + hook.future.then((data) { if (_activeCallbackIdentity == callbackIdentity) { setState(() { _snapshot = AsyncSnapshot.withData(ConnectionState.done, data); }); } - }, onError: (Object error) { + }, onError: (dynamic error) { if (_activeCallbackIdentity == callbackIdentity) { setState(() { _snapshot = AsyncSnapshot.withError(ConnectionState.done, error); @@ -108,12 +108,12 @@ AsyncSnapshot useStream(Stream stream, } class _StreamHook extends Hook> { + const _StreamHook(this.stream, {this.initialData, this.preserveState = true}); + final Stream stream; final T initialData; final bool preserveState; - _StreamHook(this.stream, {this.initialData, this.preserveState = true}); - @override _StreamHookState createState() => _StreamHookState(); } @@ -153,11 +153,11 @@ class _StreamHookState extends HookState, _StreamHook> { void _subscribe() { if (hook.stream != null) { - _subscription = hook.stream.listen((T data) { + _subscription = hook.stream.listen((data) { setState(() { _summary = afterData(_summary, data); }); - }, onError: (Object error) { + }, onError: (dynamic error) { setState(() { _summary = afterError(_summary, error); }); @@ -222,14 +222,14 @@ StreamController useStreamController( } class _StreamControllerHook extends Hook> { - final bool sync; - final VoidCallback onListen; - final VoidCallback onCancel; - const _StreamControllerHook( {this.sync = false, this.onListen, this.onCancel, List keys}) : super(keys: keys); + final bool sync; + final VoidCallback onListen; + final VoidCallback onCancel; + @override _StreamControllerHookState createState() => _StreamControllerHookState(); diff --git a/lib/src/framework.dart b/lib/src/framework.dart index 0d68a56b..589c051f 100644 --- a/lib/src/framework.dart +++ b/lib/src/framework.dart @@ -1,3 +1,5 @@ +import 'dart:collection'; + import 'package:flutter/foundation.dart'; import 'package:flutter/scheduler.dart'; import 'package:flutter/widgets.dart'; @@ -162,8 +164,9 @@ Calling them outside of build method leads to an unstable state and is therefore return false; } - var i1 = p1.iterator; - var i2 = p2.iterator; + final i1 = p1.iterator; + final i2 = p2.iterator; + // ignore: literal_only_boolean_expressions, returns will abort the loop while (true) { if (!i1.moveNext() || !i2.moveNext()) { return true; @@ -193,8 +196,8 @@ Calling them outside of build method leads to an unstable state and is therefore abstract class HookState> { /// Equivalent of [State.context] for [HookState] @protected - BuildContext get context => _element.context; - State _element; + BuildContext get context => _element; + HookElement _element; /// Equivalent of [State.widget] for [HookState] T get hook => _hook; @@ -236,35 +239,76 @@ abstract class HookState> { /// * [State.reassemble] void reassemble() {} + void markMayNeedRebuild(bool Function() shouldRebuild) { + if (_element._isOptionalRebuild == null) { + _element + .._isOptionalRebuild = true + .._shouldRebuildQueue ??= LinkedList() + .._shouldRebuildQueue.add(_Entry(shouldRebuild)) + ..markNeedsBuild(); + } + } + /// Equivalent of [State.setState] for [HookState] @protected void setState(VoidCallback fn) { - // ignore: invalid_use_of_protected_member - _element.setState(fn); + fn(); + _element + .._isOptionalRebuild = false + ..markNeedsBuild(); } } +class _Entry extends LinkedListEntry<_Entry> { + _Entry(this.value); + final T value; +} + /// An [Element] that uses a [HookWidget] as its configuration. class HookElement extends StatefulElement { - static HookElement _currentContext; - /// Creates an element that uses the given widget as its configuration. HookElement(HookWidget widget) : super(widget); + static HookElement _currentContext; Iterator _currentHook; int _hookIndex; List _hooks; + LinkedList<_Entry> _shouldRebuildQueue; + bool _isOptionalRebuild = false; + Widget _buildCache; bool _didFinishBuildOnce = false; bool _debugDidReassemble; bool _debugShouldDispose; bool _debugIsInitHook; + @override + void update(StatefulWidget newWidget) { + _isOptionalRebuild = false; + super.update(newWidget); + } + + @override + void didChangeDependencies() { + _isOptionalRebuild = false; + super.didChangeDependencies(); + } + @override HookWidget get widget => super.widget as HookWidget; @override Widget build() { + final canAbortBuild = _isOptionalRebuild == true && + _shouldRebuildQueue.any((cb) => !cb.value()); + + _isOptionalRebuild = null; + _shouldRebuildQueue?.clear(); + + if (canAbortBuild) { + return _buildCache; + } + _currentHook = _hooks?.iterator; // first iterator always has null _currentHook?.moveNext(); @@ -274,21 +318,23 @@ class HookElement extends StatefulElement { _debugIsInitHook = false; _debugDidReassemble ??= false; return true; - }()); + }(), ''); HookElement._currentContext = this; - final result = super.build(); + _buildCache = super.build(); HookElement._currentContext = null; // dispose removed items assert(() { - if (!debugHotReloadHooksEnabled) return true; + if (!debugHotReloadHooksEnabled) { + return true; + } if (_debugDidReassemble && _hooks != null) { - for (var i = _hookIndex; i < _hooks.length;) { - _hooks.removeAt(i).dispose(); + while (_hookIndex < _hooks.length) { + _hooks.removeAt(_hookIndex).dispose(); } } return true; - }()); + }(), ''); assert(_hookIndex == (_hooks?.length ?? 0), ''' Build for $widget finished with less hooks used than a previous build. Used $_hookIndex hooks while a previous build had ${_hooks.length}. @@ -296,12 +342,14 @@ This may happen if the call to `Hook.use` is made under some condition. '''); assert(() { - if (!debugHotReloadHooksEnabled) return true; + if (!debugHotReloadHooksEnabled) { + return true; + } _debugDidReassemble = false; return true; - }()); + }(), ''); _didFinishBuildOnce = true; - return result; + return _buildCache; } /// A read-only list of all hooks available. @@ -311,9 +359,13 @@ This may happen if the call to `Hook.use` is made under some condition. List get debugHooks => List.unmodifiable(_hooks); @override - T dependOnInheritedWidgetOfExactType( - {Object aspect}) { - assert(!_debugIsInitHook); + T dependOnInheritedWidgetOfExactType({ + Object aspect, + }) { + assert( + !_debugIsInitHook, + 'Cannot listen to inherited widgets inside HookState.initState. Use HookState.build instead', + ); return super.dependOnInheritedWidgetOfExactType(aspect: aspect); } @@ -389,21 +441,23 @@ This may happen if the call to `Hook.use` is made under some condition. } } return true; - }()); + }(), ''); } R _use(Hook hook) { HookState> hookState; // first build if (_currentHook == null) { - assert(_debugDidReassemble || !_didFinishBuildOnce); + assert(_debugDidReassemble || !_didFinishBuildOnce, 'No previous hook found at $_hookIndex, is a hook wrapped in a `if`?'); hookState = _createHookState(hook); _hooks ??= []; _hooks.add(hookState); } else { // recreate states on hot-reload of the order changed assert(() { - if (!debugHotReloadHooksEnabled) return true; + if (!debugHotReloadHooksEnabled) { + return true; + } if (!_debugDidReassemble) { return true; } @@ -423,13 +477,13 @@ This may happen if the call to `Hook.use` is made under some condition. hookState = _pushHook(hook); } return true; - }()); + }(), ''); if (!_didFinishBuildOnce && _currentHook.current == null) { hookState = _pushHook(hook); _currentHook.moveNext(); } else { - assert(_currentHook.current != null); - assert(_debugTypesAreRight(hook)); + assert(_currentHook.current != null, 'No previous hook found at $_hookIndex, is a hook wrapped in a `if`?'); + assert(_debugTypesAreRight(hook), ''); if (_currentHook.current.hook == hook) { hookState = _currentHook.current as HookState>; @@ -454,27 +508,27 @@ This may happen if the call to `Hook.use` is made under some condition. HookState> _replaceHookAt(int index, Hook hook) { _hooks.removeAt(_hookIndex).dispose(); - var hookState = _createHookState(hook); + final hookState = _createHookState(hook); _hooks.insert(_hookIndex, hookState); return hookState; } HookState> _insertHookAt(int index, Hook hook) { - var hookState = _createHookState(hook); + final hookState = _createHookState(hook); _hooks.insert(index, hookState); _resetsIterator(hookState); return hookState; } HookState> _pushHook(Hook hook) { - var hookState = _createHookState(hook); + final hookState = _createHookState(hook); _hooks.add(hookState); _resetsIterator(hookState); return hookState; } bool _debugTypesAreRight(Hook hook) { - assert(_currentHook.current.hook.runtimeType == hook.runtimeType); + assert(_currentHook.current.hook.runtimeType == hook.runtimeType, 'The previous and new hooks at index $_hookIndex do not match'); return true; } @@ -490,16 +544,16 @@ This may happen if the call to `Hook.use` is made under some condition. assert(() { _debugIsInitHook = true; return true; - }()); + }(), ''); final state = hook.createState() - .._element = this.state + .._element = this .._hook = hook ..initHook(); assert(() { _debugIsInitHook = false; return true; - }()); + }(), ''); return state; } @@ -548,21 +602,21 @@ BuildContext useContext() { /// A [HookWidget] that defer its [HookWidget.build] to a callback class HookBuilder extends HookWidget { - /// The callback used by [HookBuilder] to create a widget. - /// - /// If a [Hook] asks for a rebuild, [builder] will be called again. - /// [builder] must not return `null`. - final Widget Function(BuildContext context) builder; - /// Creates a widget that delegates its build to a callback. /// /// The [builder] argument must not be null. const HookBuilder({ @required this.builder, Key key, - }) : assert(builder != null), + }) : assert(builder != null, '`builder` cannot be null'), super(key: key); + /// The callback used by [HookBuilder] to create a widget. + /// + /// If a [Hook] asks for a rebuild, [builder] will be called again. + /// [builder] must not return `null`. + final Widget Function(BuildContext context) builder; + @override Widget build(BuildContext context) => builder(context); } diff --git a/lib/src/hooks.dart b/lib/src/hooks.dart index bbec06e9..45cb6731 100644 --- a/lib/src/hooks.dart +++ b/lib/src/hooks.dart @@ -3,7 +3,8 @@ import 'dart:async'; import 'package:flutter/foundation.dart'; import 'package:flutter/scheduler.dart'; import 'package:flutter/widgets.dart'; -import 'package:flutter_hooks/src/framework.dart'; + +import 'framework.dart'; part 'animation.dart'; part 'async.dart'; diff --git a/lib/src/listenable.dart b/lib/src/listenable.dart index b80db47f..f46b419a 100644 --- a/lib/src/listenable.dart +++ b/lib/src/listenable.dart @@ -21,9 +21,10 @@ T useListenable(T listenable) { } class _ListenableHook extends Hook { - final Listenable listenable; + const _ListenableHook(this.listenable) + : assert(listenable != null, 'listenable cannot be null'); - const _ListenableHook(this.listenable) : assert(listenable != null); + final Listenable listenable; @override _ListenableStateHook createState() => _ListenableStateHook(); @@ -74,11 +75,11 @@ ValueNotifier useValueNotifier([T intialData, List keys]) { } class _ValueNotifierHook extends Hook> { - final T initialData; - const _ValueNotifierHook({List keys, this.initialData}) : super(keys: keys); + final T initialData; + @override _UseValueNotiferHookState createState() => _UseValueNotiferHookState(); } diff --git a/lib/src/misc.dart b/lib/src/misc.dart index 5c34a1ae..985b78ac 100644 --- a/lib/src/misc.dart +++ b/lib/src/misc.dart @@ -43,13 +43,13 @@ Store useReducer( } class _ReducerdHook extends Hook> { + const _ReducerdHook(this.reducer, {this.initialState, this.initialAction}) + : assert(reducer != null, 'reducer cannot be null'); + final Reducer reducer; final State initialState; final Action initialAction; - const _ReducerdHook(this.reducer, {this.initialState, this.initialAction}) - : assert(reducer != null); - @override _ReducerdHookState createState() => _ReducerdHookState(); @@ -65,17 +65,16 @@ class _ReducerdHookState void initHook() { super.initHook(); state = hook.reducer(hook.initialState, hook.initialAction); - assert(state != null); + // TODO support null + assert(state != null, 'reducers cannot return null'); } @override void dispatch(Action action) { - final res = hook.reducer(state, action); - assert(res != null); - if (state != res) { - setState(() { - state = res; - }); + final newState = hook.reducer(state, action); + assert(newState != null, 'recuders cannot return null'); + if (state != newState) { + setState(() => state = newState); } } @@ -91,7 +90,7 @@ T usePrevious(T val) { } class _PreviousHook extends Hook { - _PreviousHook(this.value); + const _PreviousHook(this.value); final T value; @@ -121,13 +120,13 @@ void useReassemble(VoidCallback callback) { assert(() { Hook.use(_ReassembleHook(callback)); return true; - }()); + }(), ''); } class _ReassembleHook extends Hook { - final VoidCallback callback; + const _ReassembleHook(this.callback) : assert(callback != null, 'callback cannot be null'); - _ReassembleHook(this.callback) : assert(callback != null); + final VoidCallback callback; @override _ReassembleHookState createState() => _ReassembleHookState(); diff --git a/lib/src/primitives.dart b/lib/src/primitives.dart index 2b1e4b42..3264f665 100644 --- a/lib/src/primitives.dart +++ b/lib/src/primitives.dart @@ -15,14 +15,15 @@ T useMemoized(T Function() valueBuilder, } class _MemoizedHook extends Hook { - final T Function() valueBuilder; - - const _MemoizedHook(this.valueBuilder, - {List keys = const []}) - : assert(valueBuilder != null), - assert(keys != null), + const _MemoizedHook( + this.valueBuilder, { + List keys = const [], + }) : assert(valueBuilder != null, 'valueBuilder cannot be null'), + assert(keys != null, 'keys cannot be null'), super(keys: keys); + final T Function() valueBuilder; + @override _MemoizedHookState createState() => _MemoizedHookState(); } @@ -61,17 +62,20 @@ class _MemoizedHookState extends HookState> { /// controller.forward(); /// }); /// ``` -R useValueChanged(T value, R valueChange(T oldValue, R oldResult)) { +R useValueChanged( + T value, + R Function(T oldValue, R oldResult) valueChange, +) { return Hook.use(_ValueChangedHook(value, valueChange)); } class _ValueChangedHook extends Hook { + const _ValueChangedHook(this.value, this.valueChanged) + : assert(valueChanged != null, 'valueChanged cannot be null'); + final R Function(T oldValue, R oldResult) valueChanged; final T value; - const _ValueChangedHook(this.value, this.valueChanged) - : assert(valueChanged != null); - @override _ValueChangedHookState createState() => _ValueChangedHookState(); } @@ -128,12 +132,12 @@ void useEffect(Dispose Function() effect, [List keys]) { } class _EffectHook extends Hook { - final Dispose Function() effect; - const _EffectHook(this.effect, [List keys]) - : assert(effect != null), + : assert(effect != null, 'effect cannot be null'), super(keys: keys); + final Dispose Function() effect; + @override _EffectHookState createState() => _EffectHookState(); } @@ -207,10 +211,10 @@ ValueNotifier useState([T initialData]) { } class _StateHook extends Hook> { - final T initialData; - const _StateHook({this.initialData}); + final T initialData; + @override _StateHookState createState() => _StateHookState(); } diff --git a/lib/src/text_controller.dart b/lib/src/text_controller.dart index eb8b33be..3c6828ba 100644 --- a/lib/src/text_controller.dart +++ b/lib/src/text_controller.dart @@ -54,18 +54,20 @@ class _TextEditingControllerHookCreator { const useTextEditingController = _TextEditingControllerHookCreator(); class _TextEditingControllerHook extends Hook { - final String initialText; - final TextEditingValue initialValue; - - _TextEditingControllerHook(this.initialText, this.initialValue, - [List keys]) - : assert( + const _TextEditingControllerHook( + this.initialText, + this.initialValue, [ + List keys, + ]) : assert( initialText == null || initialValue == null, "initialText and intialValue can't both be set on a call to " 'useTextEditingController!', ), super(keys: keys); + final String initialText; + final TextEditingValue initialValue; + @override _TextEditingControllerHookState createState() { return _TextEditingControllerHookState(); diff --git a/pubspec.lock b/pubspec.lock index c1c5dd3b..ddcbf2c4 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -22,6 +22,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.1.3" + clock: + dependency: transitive + description: + name: clock + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.1" collection: dependency: transitive description: @@ -29,6 +36,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.14.12" + fake_async: + dependency: transitive + description: + name: fake_async + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.0" flutter: dependency: "direct main" description: flutter @@ -66,21 +80,7 @@ packages: name: path url: "https://pub.dartlang.org" source: hosted - version: "1.6.4" - pedantic: - dependency: "direct dev" - description: - name: pedantic - url: "https://pub.dartlang.org" - source: hosted - version: "1.9.0" - quiver: - dependency: transitive - description: - name: quiver - url: "https://pub.dartlang.org" - source: hosted - version: "2.1.3" + version: "1.7.0" sky_engine: dependency: transitive description: flutter @@ -127,7 +127,7 @@ packages: name: test_api url: "https://pub.dartlang.org" source: hosted - version: "0.2.15" + version: "0.2.16" typed_data: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 819fa585..a1e7820f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -3,7 +3,7 @@ description: A flutter implementation of React hooks. It adds a new kind of widg homepage: https://github.com/rrousselGit/flutter_hooks author: Remi Rousselet -version: 0.9.0 +version: 0.10.0-dev environment: sdk: ">=2.7.0 <3.0.0" @@ -15,5 +15,4 @@ dependencies: dev_dependencies: flutter_test: sdk: flutter - pedantic: ^1.4.0 mockito: ">=4.0.0 <5.0.0" diff --git a/test/focus_test.dart b/test/focus_test.dart index 67955652..aec2bf7f 100644 --- a/test/focus_test.dart +++ b/test/focus_test.dart @@ -17,7 +17,7 @@ void main() { // ignore: invalid_use_of_protected_member expect(focusNode.hasListeners, isFalse); - var previous = focusNode; + final previousValue = focusNode; await tester.pumpWidget( HookBuilder(builder: (_) { @@ -26,7 +26,7 @@ void main() { }), ); - expect(previous, focusNode); + expect(previousValue, focusNode); // ignore: invalid_use_of_protected_member expect(focusNode.hasListeners, isFalse); diff --git a/test/hook_builder_test.dart b/test/hook_builder_test.dart index 558f54f7..b722993d 100644 --- a/test/hook_builder_test.dart +++ b/test/hook_builder_test.dart @@ -11,7 +11,8 @@ void main() { return Container(); }); - final createBuilder = () => HookBuilder(builder: fn.call); + Widget createBuilder() => HookBuilder(builder: fn.call); + final _builder = createBuilder(); await tester.pumpWidget(_builder); diff --git a/test/hook_widget_test.dart b/test/hook_widget_test.dart index 47fdc5fd..71cff077 100644 --- a/test/hook_widget_test.dart +++ b/test/hook_widget_test.dart @@ -29,15 +29,17 @@ void main() { final reassemble = Func0(); final builder = Func1(); - final createHook = () => HookTest( - build: build.call, - dispose: dispose.call, - didUpdateHook: didUpdateHook.call, - reassemble: reassemble.call, - initHook: initHook.call, - didBuild: didBuild, - deactivate: deactivate, - ); + HookTest createHook() { + return HookTest( + build: build.call, + dispose: dispose.call, + didUpdateHook: didUpdateHook.call, + reassemble: reassemble.call, + initHook: initHook.call, + didBuild: didBuild, + deactivate: deactivate, + ); + } void verifyNoMoreHookInteration() { verifyNoMoreInteractions(build); @@ -66,98 +68,102 @@ void main() { final state = ValueNotifier(false); final deactivate1 = Func0(); final deactivate2 = Func0(); - await tester.pumpWidget(Directionality( - textDirection: TextDirection.rtl, - child: ValueListenableBuilder( + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.rtl, + child: ValueListenableBuilder( valueListenable: state, - builder: (context, bool value, _) => Stack(children: [ - Container( - key: const Key('1'), - child: HookBuilder( - key: value ? _key2 : _key1, - builder: (context) { - Hook.use(HookTest(deactivate: deactivate1)); - return Container(); - }, - ), - ), - HookBuilder( - key: !value ? _key2 : _key1, + builder: (context, value, _) { + return Stack(children: [ + Container( + key: const Key('1'), + child: HookBuilder( + key: value ? _key2 : _key1, builder: (context) { - Hook.use(HookTest(deactivate: deactivate2)); + Hook.use(HookTest(deactivate: deactivate1)); return Container(); }, ), - ])), - )); + ), + HookBuilder( + key: !value ? _key2 : _key1, + builder: (context) { + Hook.use(HookTest(deactivate: deactivate2)); + return Container(); + }, + ), + ]); + }, + ), + ), + ); + await tester.pump(); + verifyNever(deactivate1()); verifyNever(deactivate2()); state.value = true; + await tester.pump(); + verifyInOrder([ deactivate1.call(), deactivate2.call(), ]); + await tester.pump(); + verifyNoMoreInteractions(deactivate1); verifyNoMoreInteractions(deactivate2); }); testWidgets('should call other deactivates even if one fails', (tester) async { - final deactivate2 = Func0(); - final _key = GlobalKey(); final onError = Func1(); final oldOnError = FlutterError.onError; FlutterError.onError = onError; + final errorBuilder = ErrorWidget.builder; ErrorWidget.builder = Func1(); when(ErrorWidget.builder(any)).thenReturn(Container()); - try { - when(deactivate2.call()).thenThrow(42); - when(builder.call(any)).thenAnswer((invocation) { - Hook.use(createHook()); + + final deactivate = Func0(); + when(deactivate.call()).thenThrow(42); + final deactivate2 = Func0(); + + final _key = GlobalKey(); + + final widget = HookBuilder( + key: _key, + builder: (context) { + Hook.use(HookTest(deactivate: deactivate)); Hook.use(HookTest(deactivate: deactivate2)); return Container(); - }); - await tester.pumpWidget(Column( - children: [ - Row( - children: [ - HookBuilder( - key: _key, - builder: builder.call, - ) - ], - ), - Container( - child: const SizedBox(), - ), - ], - )); + }, + ); - await tester.pumpWidget(Column( - children: [ - Row( - children: [], - ), - Container( - child: HookBuilder( - key: _key, - builder: builder.call, - ), - ), - ], - )); + try { + await tester.pumpWidget(SizedBox(child: widget)); + + verifyNoMoreInteractions(deactivate); + verifyNoMoreInteractions(deactivate2); + + await tester.pumpWidget(widget); + verifyInOrder([ + deactivate(), + deactivate2(), + ]); + + verify(onError.call(any)).called(1); + verifyNoMoreInteractions(deactivate); + verifyNoMoreInteractions(deactivate2); + } finally { // reset the exception because after the test // flutter tries to deactivate the widget and it causes // and exception - when(deactivate2.call()).thenAnswer((_) {}); - verify(onError.call(any)).called((int x) => x > 1); - verify(deactivate.call()).called(1); - } finally { + when(deactivate.call()).thenAnswer((_) {}); FlutterError.onError = oldOnError; ErrorWidget.builder = errorBuilder; } @@ -176,8 +182,8 @@ void main() { testWidgets('allows using inherited widgets outside of initHook', (tester) async { when(build(any)).thenAnswer((invocation) { - invocation.positionalArguments.first as BuildContext - ..dependOnInheritedWidgetOfExactType(); + final context = invocation.positionalArguments.first as BuildContext; + context.dependOnInheritedWidgetOfExactType(); return null; }); diff --git a/test/memoized_test.dart b/test/memoized_test.dart index e49d1ce7..e2dc0281 100644 --- a/test/memoized_test.dart +++ b/test/memoized_test.dart @@ -16,7 +16,7 @@ void main() { testWidgets('invalid parameters', (tester) async { await tester.pumpWidget(HookBuilder(builder: (context) { - useMemoized(null); + useMemoized(null); return Container(); })); expect(tester.takeException(), isAssertionError); @@ -198,7 +198,7 @@ void main() { }); testWidgets( - 'memoized parameter reference do not change don\'t call valueBuilder', + "memoized parameter reference do not change don't call valueBuilder", (tester) async { int result; final parameters = []; diff --git a/test/mock.dart b/test/mock.dart index 135bf933..a2e449cd 100644 --- a/test/mock.dart +++ b/test/mock.dart @@ -1,3 +1,5 @@ +// ignore_for_file: one_member_abstracts + import 'package:flutter/widgets.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -26,15 +28,7 @@ abstract class _Func2 { class Func2 extends Mock implements _Func2 {} class HookTest extends Hook { - final R Function(BuildContext context) build; - final void Function() dispose; - final void Function() didBuild; - final void Function() initHook; - final void Function() deactivate; - final void Function(HookTest previousHook) didUpdateHook; - final void Function() reassemble; - final HookStateTest Function() createStateFn; - + // ignore: prefer_const_constructors_in_immutables HookTest({ this.build, this.dispose, @@ -47,6 +41,15 @@ class HookTest extends Hook { List keys, }) : super(keys: keys); + final R Function(BuildContext context) build; + final void Function() dispose; + final void Function() didBuild; + final void Function() initHook; + final void Function() deactivate; + final void Function(HookTest previousHook) didUpdateHook; + final void Function() reassemble; + final HookStateTest Function() createStateFn; + @override HookStateTest createState() => createStateFn != null ? createStateFn() : HookStateTest(); @@ -123,11 +126,11 @@ Element _rootOf(Element element) { void hotReload(WidgetTester tester) { final root = _rootOf(tester.allElements.first); - TestWidgetsFlutterBinding.ensureInitialized().buildOwner..reassemble(root); + TestWidgetsFlutterBinding.ensureInitialized().buildOwner.reassemble(root); } Future expectPump( - Future pump(), + Future Function() pump, dynamic matcher, { String reason, dynamic skip, diff --git a/test/use_animation_controller_test.dart b/test/use_animation_controller_test.dart index fd078c7c..0f3ddd39 100644 --- a/test/use_animation_controller_test.dart +++ b/test/use_animation_controller_test.dart @@ -68,7 +68,7 @@ void main() { expect(controller.animationBehavior, AnimationBehavior.preserve); expect(controller.debugLabel, 'Foo'); - var previousController = controller; + final previousController = controller; provider = _TickerProvider(); when(provider.createTicker(any)).thenAnswer((_) { return tester @@ -79,11 +79,7 @@ void main() { HookBuilder(builder: (context) { controller = useAnimationController( vsync: provider, - animationBehavior: AnimationBehavior.normal, duration: const Duration(seconds: 2), - initialValue: 0, - lowerBound: 0, - upperBound: 0, debugLabel: 'Bar', ); return Container(); diff --git a/test/use_animation_test.dart b/test/use_animation_test.dart index c80a0214..8b037b30 100644 --- a/test/use_animation_test.dart +++ b/test/use_animation_test.dart @@ -19,12 +19,14 @@ void main() { var listenable = AnimationController(vsync: tester); double result; - pump() => tester.pumpWidget(HookBuilder( - builder: (context) { - result = useAnimation(listenable); - return Container(); - }, - )); + Future pump() { + return tester.pumpWidget(HookBuilder( + builder: (context) { + result = useAnimation(listenable); + return Container(); + }, + )); + } await pump(); diff --git a/test/use_effect_test.dart b/test/use_effect_test.dart index 49d9ef1f..49f494a9 100644 --- a/test/use_effect_test.dart +++ b/test/use_effect_test.dart @@ -35,11 +35,13 @@ void main() { final dispose = Func0(); when(effect.call()).thenReturn(dispose.call); - builder() => HookBuilder(builder: (context) { - useEffect(effect.call); - unrelated.call(); - return Container(); - }); + Widget builder() { + return HookBuilder(builder: (context) { + useEffect(effect.call); + unrelated.call(); + return Container(); + }); + } await tester.pumpWidget(builder()); @@ -180,10 +182,12 @@ void main() { final effect = Func0(); List parameters; - builder() => HookBuilder(builder: (context) { - useEffect(effect.call, parameters); - return Container(); - }); + Widget builder() { + return HookBuilder(builder: (context) { + useEffect(effect.call, parameters); + return Container(); + }); + } parameters = ['foo']; final disposerA = Func0(); diff --git a/test/use_future_test.dart b/test/use_future_test.dart index 89389e4a..e82f3377 100644 --- a/test/use_future_test.dart +++ b/test/use_future_test.dart @@ -59,8 +59,7 @@ void main() { }; } - testWidgets('gracefully handles transition from null future', - (WidgetTester tester) async { + testWidgets('gracefully handles transition from null future', (tester) async { await tester.pumpWidget(HookBuilder(builder: snapshotText(null))); expect(find.text('AsyncSnapshot(ConnectionState.none, null, null)'), findsOneWidget); @@ -71,8 +70,7 @@ void main() { find.text('AsyncSnapshot(ConnectionState.waiting, null, null)'), findsOneWidget); }); - testWidgets('gracefully handles transition to null future', - (WidgetTester tester) async { + testWidgets('gracefully handles transition to null future', (tester) async { final completer = Completer(); await tester .pumpWidget(HookBuilder(builder: snapshotText(completer.future))); @@ -87,8 +85,7 @@ void main() { expect(find.text('AsyncSnapshot(ConnectionState.none, null, null)'), findsOneWidget); }); - testWidgets('gracefully handles transition to other future', - (WidgetTester tester) async { + testWidgets('gracefully handles transition to other future', (tester) async { final completerA = Completer(); final completerB = Completer(); await tester @@ -107,8 +104,7 @@ void main() { expect(find.text('AsyncSnapshot(ConnectionState.done, B, null)'), findsOneWidget); }); - testWidgets('tracks life-cycle of Future to success', - (WidgetTester tester) async { + testWidgets('tracks life-cycle of Future to success', (tester) async { final completer = Completer(); await tester .pumpWidget(HookBuilder(builder: snapshotText(completer.future))); @@ -121,8 +117,7 @@ void main() { find.text('AsyncSnapshot(ConnectionState.done, hello, null)'), findsOneWidget); }); - testWidgets('tracks life-cycle of Future to error', - (WidgetTester tester) async { + testWidgets('tracks life-cycle of Future to error', (tester) async { final completer = Completer(); await tester .pumpWidget(HookBuilder(builder: snapshotText(completer.future))); @@ -134,8 +129,7 @@ void main() { expect(find.text('AsyncSnapshot(ConnectionState.done, null, bad)'), findsOneWidget); }); - testWidgets('runs the builder using given initial data', - (WidgetTester tester) async { + testWidgets('runs the builder using given initial data', (tester) async { await tester.pumpWidget(HookBuilder( builder: snapshotText( null, @@ -145,8 +139,7 @@ void main() { expect(find.text('AsyncSnapshot(ConnectionState.none, I, null)'), findsOneWidget); }); - testWidgets('ignores initialData when reconfiguring', - (WidgetTester tester) async { + testWidgets('ignores initialData when reconfiguring', (tester) async { await tester.pumpWidget(HookBuilder( builder: snapshotText( null, diff --git a/test/use_listenable_test.dart b/test/use_listenable_test.dart index 6ce5caef..928cae1d 100644 --- a/test/use_listenable_test.dart +++ b/test/use_listenable_test.dart @@ -17,12 +17,14 @@ void main() { testWidgets('useListenable', (tester) async { var listenable = ValueNotifier(0); - pump() => tester.pumpWidget(HookBuilder( - builder: (context) { - useListenable(listenable); - return Container(); - }, - )); + Future pump() { + return tester.pumpWidget(HookBuilder( + builder: (context) { + useListenable(listenable); + return Container(); + }, + )); + } await pump(); diff --git a/test/use_reassemble_test.dart b/test/use_reassemble_test.dart index 2bdd84d4..ab79c91b 100644 --- a/test/use_reassemble_test.dart +++ b/test/use_reassemble_test.dart @@ -14,7 +14,7 @@ void main() { ); }); - testWidgets('hot-reload calls useReassemble\'s callback', (tester) async { + testWidgets("hot-reload calls useReassemble's callback", (tester) async { final reassemble = Func0(); await tester.pumpWidget(HookBuilder(builder: (context) { useReassemble(reassemble); diff --git a/test/use_reducer_test.dart b/test/use_reducer_test.dart index 78f3b81f..a54279ab 100644 --- a/test/use_reducer_test.dart +++ b/test/use_reducer_test.dart @@ -9,12 +9,14 @@ void main() { final reducer = Func2(); Store store; - pump() => tester.pumpWidget(HookBuilder( - builder: (context) { - store = useReducer(reducer.call); - return Container(); - }, - )); + Future pump() { + return tester.pumpWidget(HookBuilder( + builder: (context) { + store = useReducer(reducer.call); + return Container(); + }, + )); + } when(reducer.call(null, null)).thenReturn(0); await pump(); @@ -96,12 +98,14 @@ void main() { final reducer = Func2(); Store store; - pump() => tester.pumpWidget(HookBuilder( - builder: (context) { - store = useReducer(reducer.call); - return Container(); - }, - )); + Future pump() { + return tester.pumpWidget(HookBuilder( + builder: (context) { + store = useReducer(reducer.call); + return Container(); + }, + )); + } when(reducer.call(null, null)).thenReturn(42); diff --git a/test/use_stream_controller_test.dart b/test/use_stream_controller_test.dart index 943d65ca..782af5c7 100644 --- a/test/use_stream_controller_test.dart +++ b/test/use_stream_controller_test.dart @@ -38,8 +38,8 @@ void main() { expect(() => controller.onResume, throwsUnsupportedError); final previousController = controller; - final onListen = () {}; - final onCancel = () {}; + void onListen() {} + void onCancel() {} await tester.pumpWidget(HookBuilder(builder: (context) { controller = useStreamController( sync: true, @@ -75,8 +75,8 @@ void main() { expect(() => controller.onResume, throwsUnsupportedError); final previousController = controller; - final onListen = () {}; - final onCancel = () {}; + void onListen() {} + void onCancel() {} await tester.pumpWidget(HookBuilder(builder: (context) { controller = useStreamController( onCancel: onCancel, diff --git a/test/use_stream_test.dart b/test/use_stream_test.dart index e48cc827..2249592c 100644 --- a/test/use_stream_test.dart +++ b/test/use_stream_test.dart @@ -1,3 +1,5 @@ +// ignore_for_file: close_sinks + import 'dart:async'; import 'package:flutter/widgets.dart'; @@ -61,8 +63,7 @@ void main() { }; } - testWidgets('gracefully handles transition from null stream', - (WidgetTester tester) async { + testWidgets('gracefully handles transition from null stream', (tester) async { await tester.pumpWidget(HookBuilder(builder: snapshotText(null))); expect(find.text('AsyncSnapshot(ConnectionState.none, null, null)'), findsOneWidget); @@ -73,8 +74,7 @@ void main() { find.text('AsyncSnapshot(ConnectionState.waiting, null, null)'), findsOneWidget); }); - testWidgets('gracefully handles transition to null stream', - (WidgetTester tester) async { + testWidgets('gracefully handles transition to null stream', (tester) async { final controller = StreamController(); await tester .pumpWidget(HookBuilder(builder: snapshotText(controller.stream))); @@ -85,8 +85,7 @@ void main() { expect(find.text('AsyncSnapshot(ConnectionState.none, null, null)'), findsOneWidget); }); - testWidgets('gracefully handles transition to other stream', - (WidgetTester tester) async { + testWidgets('gracefully handles transition to other stream', (tester) async { final controllerA = StreamController(); final controllerB = StreamController(); await tester @@ -103,7 +102,7 @@ void main() { findsOneWidget); }); testWidgets('tracks events and errors of stream until completion', - (WidgetTester tester) async { + (tester) async { final controller = StreamController(); await tester .pumpWidget(HookBuilder(builder: snapshotText(controller.stream))); @@ -127,8 +126,7 @@ void main() { expect(find.text('AsyncSnapshot(ConnectionState.done, 4, null)'), findsOneWidget); }); - testWidgets('runs the builder using given initial data', - (WidgetTester tester) async { + testWidgets('runs the builder using given initial data', (tester) async { final controller = StreamController(); await tester.pumpWidget(HookBuilder( builder: snapshotText(controller.stream, initialData: 'I'), @@ -136,8 +134,7 @@ void main() { expect(find.text('AsyncSnapshot(ConnectionState.waiting, I, null)'), findsOneWidget); }); - testWidgets('ignores initialData when reconfiguring', - (WidgetTester tester) async { + testWidgets('ignores initialData when reconfiguring', (tester) async { await tester.pumpWidget(HookBuilder( builder: snapshotText(null, initialData: 'I'), )); diff --git a/test/use_text_editing_controller_test.dart b/test/use_text_editing_controller_test.dart index 24a484b9..32833fa8 100644 --- a/test/use_text_editing_controller_test.dart +++ b/test/use_text_editing_controller_test.dart @@ -32,9 +32,11 @@ void main() { // pump another widget so that the old one gets disposed await tester.pumpWidget(Container()); - expect(() => controller.addListener(null), throwsA((FlutterError error) { - return error.message.contains('disposed'); - })); + expect( + () => controller.addListener(null), + throwsA(isFlutterError.having( + (e) => e.message, 'message', contains('disposed'))), + ); }); testWidgets('respects initial text property', (tester) async { diff --git a/test/use_value_changed_test.dart b/test/use_value_changed_test.dart index 5bfd4a11..f99b3872 100644 --- a/test/use_value_changed_test.dart +++ b/test/use_value_changed_test.dart @@ -9,7 +9,7 @@ void main() { final _useValueChanged = Func2(); String result; - pump() => tester.pumpWidget(HookBuilder( + Future pump() => tester.pumpWidget(HookBuilder( builder: (context) { result = useValueChanged(value, _useValueChanged.call); return Container(); diff --git a/test/use_value_listenable_test.dart b/test/use_value_listenable_test.dart index d15011d2..e2e3308c 100644 --- a/test/use_value_listenable_test.dart +++ b/test/use_value_listenable_test.dart @@ -18,12 +18,14 @@ void main() { var listenable = ValueNotifier(0); int result; - pump() => tester.pumpWidget(HookBuilder( - builder: (context) { - result = useValueListenable(listenable); - return Container(); - }, - )); + Future pump() { + return tester.pumpWidget(HookBuilder( + builder: (context) { + result = useValueListenable(listenable); + return Container(); + }, + )); + } await pump(); diff --git a/test/use_value_notifier_test.dart b/test/use_value_notifier_test.dart index 29bba791..ed4f1733 100644 --- a/test/use_value_notifier_test.dart +++ b/test/use_value_notifier_test.dart @@ -110,7 +110,7 @@ void main() { expect(state, isNot(previous)); }); - testWidgets('instance stays the same when key don\' change', + testWidgets("instance stays the same when key don' change", (tester) async { ValueNotifier state; ValueNotifier previous; From 62b6e82bb1d1fd8c0af67628e5d0454ffd4c1331 Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Sun, 7 Jun 2020 04:03:37 +0100 Subject: [PATCH 117/384] Add mayNeedBuild --- test/pre_build_abort_test.dart | 188 +++++++++++++++++++++++++++++++++ 1 file changed, 188 insertions(+) create mode 100644 test/pre_build_abort_test.dart diff --git a/test/pre_build_abort_test.dart b/test/pre_build_abort_test.dart new file mode 100644 index 00000000..b3380dac --- /dev/null +++ b/test/pre_build_abort_test.dart @@ -0,0 +1,188 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/src/widgets/framework.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + testWidgets('pre-build-abort', (tester) async { + var buildCount = 0; + final notifier = ValueNotifier(0); + + await tester.pumpWidget( + HookBuilder(builder: (c) { + buildCount++; + final value = Hook.use(IsPositiveHook(notifier)); + + return Text('$value', textDirection: TextDirection.ltr); + }), + ); + + expect(buildCount, 1); + expect(find.text('true'), findsOneWidget); + expect(find.text('false'), findsNothing); + + notifier.value = -10; + + await tester.pump(); + + expect(buildCount, 2); + expect(find.text('false'), findsOneWidget); + expect(find.text('true'), findsNothing); + + notifier.value = -20; + + await tester.pump(); + + expect(buildCount, 2); + expect(find.text('false'), findsOneWidget); + expect(find.text('true'), findsNothing); + + notifier.value = 20; + + await tester.pump(); + + expect(buildCount, 3); + expect(find.text('true'), findsOneWidget); + expect(find.text('false'), findsNothing); + }); + testWidgets('setState then markMayNeedBuild still force build', + (tester) async { + var buildCount = 0; + final notifier = ValueNotifier(0); + + await tester.pumpWidget( + HookBuilder(builder: (c) { + buildCount++; + useListenable(notifier); + final value = Hook.use(IsPositiveHook(notifier)); + + return Text('$value', textDirection: TextDirection.ltr); + }), + ); + + expect(buildCount, 1); + expect(find.text('true'), findsOneWidget); + expect(find.text('false'), findsNothing); + + notifier.value++; + await tester.pump(); + + expect(buildCount, 2); + expect(find.text('true'), findsOneWidget); + expect(find.text('false'), findsNothing); + }); + testWidgets('markMayNeedBuild then widget rebuild forces build', + (tester) async { + var buildCount = 0; + final notifier = ValueNotifier(0); + + Widget build() { + return HookBuilder(builder: (c) { + buildCount++; + final value = Hook.use(IsPositiveHook(notifier)); + + return Text('$value', textDirection: TextDirection.ltr); + }); + } + + await tester.pumpWidget(build()); + + expect(buildCount, 1); + expect(find.text('true'), findsOneWidget); + expect(find.text('false'), findsNothing); + + notifier.value++; + await tester.pumpWidget(build()); + + expect(buildCount, 2); + expect(find.text('true'), findsOneWidget); + expect(find.text('false'), findsNothing); + }); + testWidgets('markMayNeedBuild then didChangeDepencies forces build', + (tester) async { + var buildCount = 0; + final notifier = ValueNotifier(0); + + final child = HookBuilder(builder: (c) { + buildCount++; + Directionality.of(c); + final value = Hook.use(IsPositiveHook(notifier)); + + return Text('$value', textDirection: TextDirection.ltr); + }); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: child, + ), + ); + + expect(buildCount, 1); + expect(find.text('true'), findsOneWidget); + expect(find.text('false'), findsNothing); + + notifier.value++; + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.rtl, + child: child, + ), + ); + + expect(buildCount, 2); + expect(find.text('true'), findsOneWidget); + expect(find.text('false'), findsNothing); + }); +} + +class IsPositiveHook extends Hook { + const IsPositiveHook(this.notifier); + + final ValueNotifier notifier; + + @override + IsPositiveHookState createState() { + return IsPositiveHookState(); + } +} + +class IsPositiveHookState extends HookState { + bool dirty = true; + bool value; + + @override + void initHook() { + super.initHook(); + value = hook.notifier.value >= 0; + hook.notifier.addListener(listener); + } + + void listener() { + dirty = true; + markMayNeedRebuild(() { + dirty = false; + final newValue = hook.notifier.value >= 0; + if (newValue != value) { + value = newValue; + return true; + } + return false; + }); + } + + @override + bool build(BuildContext context) { + if (dirty) { + dirty = false; + value = hook.notifier.value >= 0; + } + return value; + } + + @override + void dispose() { + hook.notifier.removeListener(listener); + super.dispose(); + } +} From 508fa3df32b50081534a86f51254c2a714ce7296 Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Sun, 7 Jun 2020 04:10:25 +0100 Subject: [PATCH 118/384] format --- lib/src/framework.dart | 9 ++++++--- lib/src/misc.dart | 3 ++- test/use_value_notifier_test.dart | 3 +-- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/lib/src/framework.dart b/lib/src/framework.dart index 589c051f..3ce1ad1c 100644 --- a/lib/src/framework.dart +++ b/lib/src/framework.dart @@ -448,7 +448,8 @@ This may happen if the call to `Hook.use` is made under some condition. HookState> hookState; // first build if (_currentHook == null) { - assert(_debugDidReassemble || !_didFinishBuildOnce, 'No previous hook found at $_hookIndex, is a hook wrapped in a `if`?'); + assert(_debugDidReassemble || !_didFinishBuildOnce, + 'No previous hook found at $_hookIndex, is a hook wrapped in a `if`?'); hookState = _createHookState(hook); _hooks ??= []; _hooks.add(hookState); @@ -482,7 +483,8 @@ This may happen if the call to `Hook.use` is made under some condition. hookState = _pushHook(hook); _currentHook.moveNext(); } else { - assert(_currentHook.current != null, 'No previous hook found at $_hookIndex, is a hook wrapped in a `if`?'); + assert(_currentHook.current != null, + 'No previous hook found at $_hookIndex, is a hook wrapped in a `if`?'); assert(_debugTypesAreRight(hook), ''); if (_currentHook.current.hook == hook) { @@ -528,7 +530,8 @@ This may happen if the call to `Hook.use` is made under some condition. } bool _debugTypesAreRight(Hook hook) { - assert(_currentHook.current.hook.runtimeType == hook.runtimeType, 'The previous and new hooks at index $_hookIndex do not match'); + assert(_currentHook.current.hook.runtimeType == hook.runtimeType, + 'The previous and new hooks at index $_hookIndex do not match'); return true; } diff --git a/lib/src/misc.dart b/lib/src/misc.dart index 985b78ac..c7d6d3ac 100644 --- a/lib/src/misc.dart +++ b/lib/src/misc.dart @@ -124,7 +124,8 @@ void useReassemble(VoidCallback callback) { } class _ReassembleHook extends Hook { - const _ReassembleHook(this.callback) : assert(callback != null, 'callback cannot be null'); + const _ReassembleHook(this.callback) + : assert(callback != null, 'callback cannot be null'); final VoidCallback callback; diff --git a/test/use_value_notifier_test.dart b/test/use_value_notifier_test.dart index ed4f1733..4bc660e1 100644 --- a/test/use_value_notifier_test.dart +++ b/test/use_value_notifier_test.dart @@ -110,8 +110,7 @@ void main() { expect(state, isNot(previous)); }); - testWidgets("instance stays the same when key don' change", - (tester) async { + testWidgets("instance stays the same when key don' change", (tester) async { ValueNotifier state; ValueNotifier previous; From 594ef877e15e49d8056f3531ce9b1cae54d1cb18 Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Fri, 12 Jun 2020 09:30:18 +0100 Subject: [PATCH 119/384] wip --- example/.flutter-plugins-dependencies | 2 +- lib/src/framework.dart | 10 +- pubspec.lock | 146 -------------------------- test/pre_build_abort_test.dart | 11 +- 4 files changed, 15 insertions(+), 154 deletions(-) delete mode 100644 pubspec.lock diff --git a/example/.flutter-plugins-dependencies b/example/.flutter-plugins-dependencies index 80366350..1cdfcc0d 100644 --- a/example/.flutter-plugins-dependencies +++ b/example/.flutter-plugins-dependencies @@ -1 +1 @@ -{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"shared_preferences","path":"/Users/remiguest/.pub-cache/hosted/pub.dartlang.org/shared_preferences-0.4.3/","dependencies":[]}],"android":[{"name":"shared_preferences","path":"/Users/remiguest/.pub-cache/hosted/pub.dartlang.org/shared_preferences-0.4.3/","dependencies":[]}],"macos":[],"linux":[],"windows":[],"web":[]},"dependencyGraph":[{"name":"shared_preferences","dependencies":[]}],"date_created":"2020-06-07 03:18:10.917665","version":"1.19.0-4.0.pre.73"} \ No newline at end of file +{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"shared_preferences","path":"/Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/shared_preferences-0.4.3/","dependencies":[]}],"android":[{"name":"shared_preferences","path":"/Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/shared_preferences-0.4.3/","dependencies":[]}],"macos":[],"linux":[],"windows":[],"web":[]},"dependencyGraph":[{"name":"shared_preferences","dependencies":[]}],"date_created":"2020-06-12 09:30:06.628061","version":"1.19.0-4.0.pre.128"} \ No newline at end of file diff --git a/lib/src/framework.dart b/lib/src/framework.dart index 3ce1ad1c..ed7da5a4 100644 --- a/lib/src/framework.dart +++ b/lib/src/framework.dart @@ -239,7 +239,9 @@ abstract class HookState> { /// * [State.reassemble] void reassemble() {} - void markMayNeedRebuild(bool Function() shouldRebuild) { + bool shouldRebuild() => true; + + void markMayNeedRebuild() { if (_element._isOptionalRebuild == null) { _element .._isOptionalRebuild = true @@ -299,13 +301,13 @@ class HookElement extends StatefulElement { @override Widget build() { - final canAbortBuild = _isOptionalRebuild == true && - _shouldRebuildQueue.any((cb) => !cb.value()); + final mustRebuild = _isOptionalRebuild != true || + _shouldRebuildQueue.any((cb) => cb.value()); _isOptionalRebuild = null; _shouldRebuildQueue?.clear(); - if (canAbortBuild) { + if (!mustRebuild) { return _buildCache; } diff --git a/pubspec.lock b/pubspec.lock deleted file mode 100644 index ddcbf2c4..00000000 --- a/pubspec.lock +++ /dev/null @@ -1,146 +0,0 @@ -# Generated by pub -# See https://dart.dev/tools/pub/glossary#lockfile -packages: - async: - dependency: transitive - description: - name: async - url: "https://pub.dartlang.org" - source: hosted - version: "2.4.1" - boolean_selector: - dependency: transitive - description: - name: boolean_selector - url: "https://pub.dartlang.org" - source: hosted - version: "2.0.0" - charcode: - dependency: transitive - description: - name: charcode - url: "https://pub.dartlang.org" - source: hosted - version: "1.1.3" - clock: - dependency: transitive - description: - name: clock - url: "https://pub.dartlang.org" - source: hosted - version: "1.0.1" - collection: - dependency: transitive - description: - name: collection - url: "https://pub.dartlang.org" - source: hosted - version: "1.14.12" - fake_async: - dependency: transitive - description: - name: fake_async - url: "https://pub.dartlang.org" - source: hosted - version: "1.1.0" - flutter: - dependency: "direct main" - description: flutter - source: sdk - version: "0.0.0" - flutter_test: - dependency: "direct dev" - description: flutter - source: sdk - version: "0.0.0" - matcher: - dependency: transitive - description: - name: matcher - url: "https://pub.dartlang.org" - source: hosted - version: "0.12.6" - meta: - dependency: transitive - description: - name: meta - url: "https://pub.dartlang.org" - source: hosted - version: "1.1.8" - mockito: - dependency: "direct dev" - description: - name: mockito - url: "https://pub.dartlang.org" - source: hosted - version: "4.1.1" - path: - dependency: transitive - description: - name: path - url: "https://pub.dartlang.org" - source: hosted - version: "1.7.0" - sky_engine: - dependency: transitive - description: flutter - source: sdk - version: "0.0.99" - source_span: - dependency: transitive - description: - name: source_span - url: "https://pub.dartlang.org" - source: hosted - version: "1.7.0" - stack_trace: - dependency: transitive - description: - name: stack_trace - url: "https://pub.dartlang.org" - source: hosted - version: "1.9.3" - stream_channel: - dependency: transitive - description: - name: stream_channel - url: "https://pub.dartlang.org" - source: hosted - version: "2.0.0" - string_scanner: - dependency: transitive - description: - name: string_scanner - url: "https://pub.dartlang.org" - source: hosted - version: "1.0.5" - term_glyph: - dependency: transitive - description: - name: term_glyph - url: "https://pub.dartlang.org" - source: hosted - version: "1.1.0" - test_api: - dependency: transitive - description: - name: test_api - url: "https://pub.dartlang.org" - source: hosted - version: "0.2.16" - typed_data: - dependency: transitive - description: - name: typed_data - url: "https://pub.dartlang.org" - source: hosted - version: "1.1.6" - vector_math: - dependency: transitive - description: - name: vector_math - url: "https://pub.dartlang.org" - source: hosted - version: "2.0.8" -sdks: - dart: ">=2.7.0 <3.0.0" diff --git a/test/pre_build_abort_test.dart b/test/pre_build_abort_test.dart index b3380dac..893ef5fb 100644 --- a/test/pre_build_abort_test.dart +++ b/test/pre_build_abort_test.dart @@ -160,15 +160,20 @@ class IsPositiveHookState extends HookState { void listener() { dirty = true; - markMayNeedRebuild(() { + markMayNeedRebuild(); + } + + @override + bool shouldRebuild() { + if (dirty) { dirty = false; final newValue = hook.notifier.value >= 0; if (newValue != value) { value = newValue; return true; } - return false; - }); + } + return false; } @override From 227f90968649f5ecf2f44a44fc0b2d1fe8b1ffb6 Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Fri, 12 Jun 2020 15:59:47 +0100 Subject: [PATCH 120/384] Finish markMayNeedRebuild --- .gitignore | 1 + lib/src/framework.dart | 18 ++++++++- test/pre_build_abort_test.dart | 73 ++++++++++++++++++++++++++++++++++ 3 files changed, 91 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 0e13fdfe..884c2afd 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,4 @@ build/ android/ ios/ /coverage +.pubspec.lock \ No newline at end of file diff --git a/lib/src/framework.dart b/lib/src/framework.dart index ed7da5a4..3d68c216 100644 --- a/lib/src/framework.dart +++ b/lib/src/framework.dart @@ -239,10 +239,26 @@ abstract class HookState> { /// * [State.reassemble] void reassemble() {} + /// Called before a [build] triggered by [markMayNeedRebuild]. + /// + /// If [shouldRebuild] returns `false` on all the hooks that called [markMayNeedRebuild] + /// then this aborts the rebuild of the associated [HookWidget]. + /// + /// There is no guarantee that this method will be called after [markMayNeedRebuild] + /// was called. + /// Some situations where [shouldRebuild] will not be called: + /// + /// - [setState] was called + /// - a previous hook's [shouldRebuild] returned `true` + /// - the associated [HookWidget] changed. bool shouldRebuild() => true; + /// Mark the associated [HookWidget] as **potentially** needing to rebuild. + /// + /// As opposed to [setState], the rebuild is optional and can be cancelled right + /// before [HookWidget.build] is called, by having [shouldRebuild] return false. void markMayNeedRebuild() { - if (_element._isOptionalRebuild == null) { + if (_element._isOptionalRebuild != false) { _element .._isOptionalRebuild = true .._shouldRebuildQueue ??= LinkedList() diff --git a/test/pre_build_abort_test.dart b/test/pre_build_abort_test.dart index 893ef5fb..e9c2c29f 100644 --- a/test/pre_build_abort_test.dart +++ b/test/pre_build_abort_test.dart @@ -3,7 +3,57 @@ import 'package:flutter/src/widgets/framework.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'mock.dart'; + void main() { + testWidgets('can queue multiple mayRebuilds at once', (tester) async { + final firstSpy = ShouldRebuildMock(); + final secondSpy = ShouldRebuildMock(); + MayRebuildState first; + MayRebuildState second; + var buildCount = 0; + + await tester.pumpWidget( + HookBuilder(builder: (c) { + buildCount++; + first = Hook.use(MayRebuild(firstSpy)); + second = Hook.use(MayRebuild(secondSpy)); + + return Container(); + }), + ); + + verifyNoMoreInteractions(firstSpy); + verifyNoMoreInteractions(secondSpy); + expect(buildCount, 1); + + first.markMayNeedRebuild(); + when(firstSpy()).thenReturn(false); + second.markMayNeedRebuild(); + when(secondSpy()).thenReturn(false); + + await tester.pump(); + + expect(buildCount, 1); + verifyInOrder([ + firstSpy(), + secondSpy(), + ]); + verifyNoMoreInteractions(firstSpy); + verifyNoMoreInteractions(secondSpy); + + first.markMayNeedRebuild(); + when(firstSpy()).thenReturn(true); + second.markMayNeedRebuild(); + when(secondSpy()).thenReturn(false); + + await tester.pump(); + + expect(buildCount, 2); + verify(firstSpy()).called(1); + verifyNoMoreInteractions(firstSpy); + verifyNoMoreInteractions(secondSpy); + }); testWidgets('pre-build-abort', (tester) async { var buildCount = 0; final notifier = ValueNotifier(0); @@ -191,3 +241,26 @@ class IsPositiveHookState extends HookState { super.dispose(); } } + +class MayRebuild extends Hook { + const MayRebuild(this.shouldRebuild); + + final ShouldRebuildMock shouldRebuild; + + @override + MayRebuildState createState() { + return MayRebuildState(); + } +} + +class MayRebuildState extends HookState { + @override + bool shouldRebuild() => hook.shouldRebuild(); + + @override + MayRebuildState build(BuildContext context) => this; +} + +class ShouldRebuildMock extends Mock { + bool call(); +} From be570ac29f2de9c2e6fd924a0c54423ac55c5c2a Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Fri, 12 Jun 2020 16:16:46 +0100 Subject: [PATCH 121/384] lint --- test/use_effect_test.dart | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/test/use_effect_test.dart b/test/use_effect_test.dart index 49f494a9..d9578601 100644 --- a/test/use_effect_test.dart +++ b/test/use_effect_test.dart @@ -3,17 +3,17 @@ import 'package:flutter_hooks/flutter_hooks.dart'; import 'mock.dart'; -final effect = Func0(); -final unrelated = Func0(); -List parameters; +void main() { + final effect = Func0(); + final unrelated = Func0(); + List parameters; -Widget builder() => HookBuilder(builder: (context) { - useEffect(effect.call, parameters); - unrelated.call(); - return Container(); - }); + Widget builder() => HookBuilder(builder: (context) { + useEffect(effect.call, parameters); + unrelated.call(); + return Container(); + }); -void main() { tearDown(() { parameters = null; reset(unrelated); From b56bb8f669533afd0b8eee605eea8728a6c112ba Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Fri, 12 Jun 2020 16:41:11 +0100 Subject: [PATCH 122/384] coverage --- test/pre_build_abort_test.dart | 29 +++++++++++++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/test/pre_build_abort_test.dart b/test/pre_build_abort_test.dart index e9c2c29f..c076e0dd 100644 --- a/test/pre_build_abort_test.dart +++ b/test/pre_build_abort_test.dart @@ -6,6 +6,26 @@ import 'package:flutter_test/flutter_test.dart'; import 'mock.dart'; void main() { + testWidgets('shouldRebuild defaults to true', (tester) async { + MayRebuildState first; + var buildCount = 0; + + await tester.pumpWidget( + HookBuilder(builder: (c) { + buildCount++; + first = Hook.use(const MayRebuild()); + + return Container(); + }), + ); + + expect(buildCount, 1); + + first.markMayNeedRebuild(); + await tester.pump(); + + expect(buildCount, 2); + }); testWidgets('can queue multiple mayRebuilds at once', (tester) async { final firstSpy = ShouldRebuildMock(); final secondSpy = ShouldRebuildMock(); @@ -243,7 +263,7 @@ class IsPositiveHookState extends HookState { } class MayRebuild extends Hook { - const MayRebuild(this.shouldRebuild); + const MayRebuild([this.shouldRebuild]); final ShouldRebuildMock shouldRebuild; @@ -255,7 +275,12 @@ class MayRebuild extends Hook { class MayRebuildState extends HookState { @override - bool shouldRebuild() => hook.shouldRebuild(); + bool shouldRebuild() { + if (hook.shouldRebuild == null) { + return super.shouldRebuild(); + } + return hook.shouldRebuild(); + } @override MayRebuildState build(BuildContext context) => this; From e55c3df1e023092c555ca019f6a3cb51cc237174 Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Fri, 12 Jun 2020 16:51:17 +0100 Subject: [PATCH 123/384] Revert "Pre build abort" --- .gitignore | 1 - all_lint_rules.yaml | 167 ------------ analysis_options.yaml | 130 +++++---- example/.flutter-plugins-dependencies | 2 +- example/analysis_options.yaml | 2 - example/lib/custom_hook_function.dart | 4 +- example/lib/main.dart | 13 +- example/lib/star_wars/models.dart | 19 +- example/lib/star_wars/planet_screen.dart | 27 +- example/lib/star_wars/redux.dart | 26 +- example/lib/star_wars/star_wars_api.dart | 8 +- example/lib/use_effect.dart | 8 +- example/lib/use_state.dart | 2 +- example/pubspec.lock | 20 +- lib/src/animation.dart | 39 ++- lib/src/async.dart | 24 +- lib/src/framework.dart | 155 +++-------- lib/src/hooks.dart | 3 +- lib/src/listenable.dart | 9 +- lib/src/misc.dart | 28 +- lib/src/primitives.dart | 34 ++- lib/src/text_controller.dart | 14 +- pubspec.lock | 146 +++++++++++ pubspec.yaml | 3 +- test/focus_test.dart | 4 +- test/hook_builder_test.dart | 3 +- test/hook_widget_test.dart | 144 +++++----- test/memoized_test.dart | 4 +- test/mock.dart | 25 +- test/pre_build_abort_test.dart | 291 --------------------- test/use_animation_controller_test.dart | 6 +- test/use_animation_test.dart | 14 +- test/use_effect_test.dart | 40 ++- test/use_future_test.dart | 21 +- test/use_listenable_test.dart | 14 +- test/use_reassemble_test.dart | 2 +- test/use_reducer_test.dart | 28 +- test/use_stream_controller_test.dart | 8 +- test/use_stream_test.dart | 19 +- test/use_text_editing_controller_test.dart | 8 +- test/use_value_changed_test.dart | 2 +- test/use_value_listenable_test.dart | 14 +- test/use_value_notifier_test.dart | 3 +- 43 files changed, 559 insertions(+), 975 deletions(-) delete mode 100644 all_lint_rules.yaml create mode 100644 pubspec.lock delete mode 100644 test/pre_build_abort_test.dart diff --git a/.gitignore b/.gitignore index 884c2afd..0e13fdfe 100644 --- a/.gitignore +++ b/.gitignore @@ -9,4 +9,3 @@ build/ android/ ios/ /coverage -.pubspec.lock \ No newline at end of file diff --git a/all_lint_rules.yaml b/all_lint_rules.yaml deleted file mode 100644 index 238664b5..00000000 --- a/all_lint_rules.yaml +++ /dev/null @@ -1,167 +0,0 @@ -linter: - rules: - - always_declare_return_types - - always_put_control_body_on_new_line - - always_put_required_named_parameters_first - - always_require_non_null_named_parameters - - always_specify_types - - annotate_overrides - - avoid_annotating_with_dynamic - - avoid_as - - avoid_bool_literals_in_conditional_expressions - - avoid_catches_without_on_clauses - - avoid_catching_errors - - avoid_classes_with_only_static_members - - avoid_double_and_int_checks - - avoid_empty_else - - avoid_equals_and_hash_code_on_mutable_classes - - avoid_escaping_inner_quotes - - avoid_field_initializers_in_const_classes - - avoid_function_literals_in_foreach_calls - - avoid_implementing_value_types - - avoid_init_to_null - - avoid_js_rounded_ints - - avoid_null_checks_in_equality_operators - - avoid_positional_boolean_parameters - - avoid_print - - avoid_private_typedef_functions - - avoid_redundant_argument_values - - avoid_relative_lib_imports - - avoid_renaming_method_parameters - - avoid_return_types_on_setters - - avoid_returning_null - - avoid_returning_null_for_future - - avoid_returning_null_for_void - - avoid_returning_this - - avoid_setters_without_getters - - avoid_shadowing_type_parameters - - avoid_single_cascade_in_expression_statements - - avoid_slow_async_io - - avoid_types_as_parameter_names - - avoid_types_on_closure_parameters - - avoid_unnecessary_containers - - avoid_unused_constructor_parameters - - avoid_void_async - - avoid_web_libraries_in_flutter - - await_only_futures - - camel_case_extensions - - camel_case_types - - cancel_subscriptions - - cascade_invocations - - close_sinks - - comment_references - - constant_identifier_names - - control_flow_in_finally - - curly_braces_in_flow_control_structures - - diagnostic_describe_all_properties - - directives_ordering - - empty_catches - - empty_constructor_bodies - - empty_statements - - file_names - - flutter_style_todos - - hash_and_equals - - implementation_imports - - invariant_booleans - - iterable_contains_unrelated_type - - join_return_with_assignment - - leading_newlines_in_multiline_strings - - library_names - - library_prefixes - - lines_longer_than_80_chars - - list_remove_unrelated_type - - literal_only_boolean_expressions - - missing_whitespace_between_adjacent_strings - - no_adjacent_strings_in_list - - no_duplicate_case_values - - no_logic_in_create_state - - no_runtimeType_toString - - non_constant_identifier_names - - null_closures - - omit_local_variable_types - - one_member_abstracts - - only_throw_errors - - overridden_fields - - package_api_docs - - package_names - - package_prefixed_library_names - - parameter_assignments - - prefer_adjacent_string_concatenation - - prefer_asserts_in_initializer_lists - - prefer_asserts_with_message - - prefer_collection_literals - - prefer_conditional_assignment - - prefer_const_constructors - - prefer_const_constructors_in_immutables - - prefer_const_declarations - - prefer_const_literals_to_create_immutables - - prefer_constructors_over_static_methods - - prefer_contains - - prefer_double_quotes - - prefer_equal_for_default_values - - prefer_expression_function_bodies - - prefer_final_fields - - prefer_final_in_for_each - - prefer_final_locals - - prefer_for_elements_to_map_fromIterable - - prefer_foreach - - prefer_function_declarations_over_variables - - prefer_generic_function_type_aliases - - prefer_if_elements_to_conditional_expressions - - prefer_if_null_operators - - prefer_initializing_formals - - prefer_inlined_adds - - prefer_int_literals - - prefer_interpolation_to_compose_strings - - prefer_is_empty - - prefer_is_not_empty - - prefer_is_not_operator - - prefer_iterable_whereType - - prefer_mixin - - prefer_null_aware_operators - - prefer_relative_imports - - prefer_single_quotes - - prefer_spread_collections - - prefer_typing_uninitialized_variables - - prefer_void_to_null - - provide_deprecation_message - - public_member_api_docs - - recursive_getters - - slash_for_doc_comments - - sort_child_properties_last - - sort_constructors_first - - sort_pub_dependencies - - sort_unnamed_constructors_first - - test_types_in_equals - - throw_in_finally - - type_annotate_public_apis - - type_init_formals - - unawaited_futures - - unnecessary_await_in_return - - unnecessary_brace_in_string_interps - - unnecessary_const - - unnecessary_final - - unnecessary_getters_setters - - unnecessary_lambdas - - unnecessary_new - - unnecessary_null_aware_assignments - - unnecessary_null_in_if_null_operators - - unnecessary_overrides - - unnecessary_parenthesis - - unnecessary_raw_strings - - unnecessary_statements - - unnecessary_string_escapes - - unnecessary_string_interpolations - - unnecessary_this - - unrelated_type_equality_checks - - unsafe_html - - use_full_hex_values_for_flutter_colors - - use_function_type_syntax_for_parameters - - use_key_in_widget_constructors - - use_raw_strings - - use_rethrow_when_possible - - use_setters_to_change_properties - - use_string_buffers - - use_to_and_as_if_applicable - - valid_regexps - - void_checks \ No newline at end of file diff --git a/analysis_options.yaml b/analysis_options.yaml index dc5b2590..cd7a9506 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -1,64 +1,80 @@ -include: all_lint_rules.yaml +include: package:pedantic/analysis_options.yaml analyzer: - exclude: - - "**/*.g.dart" - - "**/*.freezed.dart" strong-mode: implicit-casts: false implicit-dynamic: false errors: - # Otherwise cause the import of all_lint_rules to warn because of some rules conflicts. - # We explicitly enabled even conflicting rules and are fixing the conflict - # in this file - included_file_warning: ignore - - # Causes false positives (https://github.com/dart-lang/sdk/issues/41571 - top_level_function_literal_block: ignore - + todo: error + include_file_not_found: ignore linter: rules: - # Personal preference. I don't find it more readable - cascade_invocations: false - - # Conflicts with `prefer_single_quotes` - # Single quotes are easier to type and don't compromise on readability. - prefer_double_quotes: false - - # Conflicts with `omit_local_variable_types` and other rules. - # As per Dart guidelines, we want to avoid unnecessary types to make the code - # more readable. - # See https://dart.dev/guides/language/effective-dart/design#avoid-type-annotating-initialized-local-variables - always_specify_types: false - - # Incompatible with `prefer_final_locals` - # Having immutable local variables makes larger functions more predictible - # so we will use `prefer_final_locals` instead. - unnecessary_final: false - - # Not quite suitable for Flutter, which may have a `build` method with a single - # return, but that return is still complex enough that a "body" is worth it. - prefer_expression_function_bodies: false - - # Conflicts with the convention used by flutter, which puts `Key key` - # and `@required Widget child` last. - always_put_required_named_parameters_first: false - - # `as` is not that bad (especially with the upcoming non-nullable types). - # Explicit exceptions is better than implicit exceptions. - avoid_as: false - - # This project doesn't use Flutter-style todos - flutter_style_todos: false - - # There are situations where we voluntarily want to catch everything, - # especially as a library. - avoid_catches_without_on_clauses: false - - # Boring as it sometimes force a line of 81 characters to be split in two. - # As long as we try to respect that 80 characters limit, going slightly - # above is fine. - lines_longer_than_80_chars: false - - # Conflicts with disabling `implicit-dynamic` - avoid_annotating_with_dynamic: false - + - public_member_api_docs + - annotate_overrides + - avoid_empty_else + - avoid_function_literals_in_foreach_calls + - avoid_init_to_null + - avoid_null_checks_in_equality_operators + - avoid_relative_lib_imports + - avoid_renaming_method_parameters + - avoid_return_types_on_setters + - avoid_returning_null + - avoid_types_as_parameter_names + - avoid_unused_constructor_parameters + - await_only_futures + - camel_case_types + - cancel_subscriptions + - cascade_invocations + - comment_references + - constant_identifier_names + - control_flow_in_finally + - directives_ordering + - empty_catches + - empty_constructor_bodies + - empty_statements + - hash_and_equals + - implementation_imports + - invariant_booleans + - iterable_contains_unrelated_type + - library_names + - library_prefixes + - list_remove_unrelated_type + - no_adjacent_strings_in_list + - no_duplicate_case_values + - non_constant_identifier_names + - null_closures + - omit_local_variable_types + - only_throw_errors + - overridden_fields + - package_api_docs + - package_names + - package_prefixed_library_names + - prefer_adjacent_string_concatenation + - prefer_collection_literals + - prefer_conditional_assignment + - prefer_const_constructors + - prefer_contains + - prefer_equal_for_default_values + - prefer_final_fields + - prefer_initializing_formals + - prefer_interpolation_to_compose_strings + - prefer_is_empty + - prefer_is_not_empty + - prefer_single_quotes + - prefer_typing_uninitialized_variables + - recursive_getters + - slash_for_doc_comments + - test_types_in_equals + - throw_in_finally + - type_init_formals + - unawaited_futures + - unnecessary_brace_in_string_interps + - unnecessary_const + - unnecessary_getters_setters + - unnecessary_lambdas + - unnecessary_new + - unnecessary_null_aware_assignments + - unnecessary_statements + - unnecessary_this + - unrelated_type_equality_checks + - use_rethrow_when_possible + - valid_regexps diff --git a/example/.flutter-plugins-dependencies b/example/.flutter-plugins-dependencies index 1cdfcc0d..04a64a12 100644 --- a/example/.flutter-plugins-dependencies +++ b/example/.flutter-plugins-dependencies @@ -1 +1 @@ -{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"shared_preferences","path":"/Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/shared_preferences-0.4.3/","dependencies":[]}],"android":[{"name":"shared_preferences","path":"/Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/shared_preferences-0.4.3/","dependencies":[]}],"macos":[],"linux":[],"windows":[],"web":[]},"dependencyGraph":[{"name":"shared_preferences","dependencies":[]}],"date_created":"2020-06-12 09:30:06.628061","version":"1.19.0-4.0.pre.128"} \ No newline at end of file +{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"shared_preferences","path":"/Users/remiguest/.pub-cache/hosted/pub.dartlang.org/shared_preferences-0.4.3/","dependencies":[]}],"android":[{"name":"shared_preferences","path":"/Users/remiguest/.pub-cache/hosted/pub.dartlang.org/shared_preferences-0.4.3/","dependencies":[]}],"macos":[],"linux":[],"windows":[],"web":[]},"dependencyGraph":[{"name":"shared_preferences","dependencies":[]}],"date_created":"2020-04-11 11:04:13.397414","version":"1.18.0-5.0.pre.57"} \ No newline at end of file diff --git a/example/analysis_options.yaml b/example/analysis_options.yaml index f52cc3f8..b6d5666f 100644 --- a/example/analysis_options.yaml +++ b/example/analysis_options.yaml @@ -9,5 +9,3 @@ analyzer: linter: rules: public_member_api_docs: false - avoid_print: false - use_key_in_widget_constructors: false diff --git a/example/lib/custom_hook_function.dart b/example/lib/custom_hook_function.dart index 168de53c..cbdfb7d0 100644 --- a/example/lib/custom_hook_function.dart +++ b/example/lib/custom_hook_function.dart @@ -21,11 +21,11 @@ class CustomHookFunctionExample extends HookWidget { child: Text('Button tapped ${counter.value} times'), ), floatingActionButton: FloatingActionButton( + child: const Icon(Icons.add), // When the button is pressed, update the value of the counter! This // will trigger a rebuild as well as printing the latest value to the // console! onPressed: () => counter.value++, - child: const Icon(Icons.add), ), ); } @@ -39,7 +39,7 @@ ValueNotifier useLoggedState([T initialData]) { final result = useState(initialData); // Next, call the useValueChanged hook to print the state whenever it changes - useValueChanged(result.value, (_, __) { + useValueChanged(result.value, (T _, T __) { print(result.value); }); diff --git a/example/lib/main.dart b/example/lib/main.dart index 9570ada7..96f2385c 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -1,10 +1,9 @@ // ignore_for_file: omit_local_variable_types import 'package:flutter/material.dart'; - -import 'star_wars/planet_screen.dart'; -import 'use_effect.dart'; -import 'use_state.dart'; -import 'use_stream.dart'; +import 'package:flutter_hooks_gallery/star_wars/planet_screen.dart'; +import 'package:flutter_hooks_gallery/use_effect.dart'; +import 'package:flutter_hooks_gallery/use_state.dart'; +import 'package:flutter_hooks_gallery/use_stream.dart'; void main() => runApp(HooksGalleryApp()); @@ -44,11 +43,11 @@ class HooksGalleryApp extends StatelessWidget { } class _GalleryItem extends StatelessWidget { - const _GalleryItem({this.title, this.builder}); - final String title; final WidgetBuilder builder; + const _GalleryItem({this.title, this.builder}); + @override Widget build(BuildContext context) { return ListTile( diff --git a/example/lib/star_wars/models.dart b/example/lib/star_wars/models.dart index fe2370ff..6cb32683 100644 --- a/example/lib/star_wars/models.dart +++ b/example/lib/star_wars/models.dart @@ -4,7 +4,6 @@ import 'package:built_collection/built_collection.dart'; import 'package:built_value/built_value.dart'; import 'package:built_value/serializer.dart'; import 'package:built_value/standard_json_plugin.dart'; -import 'package:meta/meta.dart'; part 'models.g.dart'; @@ -16,14 +15,13 @@ part 'models.g.dart'; final Serializers serializers = (_$serializers.toBuilder()..addPlugin(StandardJsonPlugin())).build(); -@immutable +/// equals one page abstract class PlanetPageModel implements Built { - factory PlanetPageModel([ - void Function(PlanetPageModelBuilder) updates, - ]) = _$PlanetPageModel; + PlanetPageModel._(); - const PlanetPageModel._(); + factory PlanetPageModel([void Function(PlanetPageModelBuilder) updates]) = + _$PlanetPageModel; static Serializer get serializer => _$planetPageModelSerializer; @@ -37,13 +35,12 @@ abstract class PlanetPageModel BuiltList get results; } -@immutable +/// equals one planet abstract class PlanetModel implements Built { - factory PlanetModel([ - void Function(PlanetModelBuilder) updates, - ]) = _$PlanetModel; + PlanetModel._(); - const PlanetModel._(); + factory PlanetModel([void Function(PlanetModelBuilder) updates]) = + _$PlanetModel; static Serializer get serializer => _$planetModelSerializer; diff --git a/example/lib/star_wars/planet_screen.dart b/example/lib/star_wars/planet_screen.dart index 959886a5..276dd36a 100644 --- a/example/lib/star_wars/planet_screen.dart +++ b/example/lib/star_wars/planet_screen.dart @@ -1,10 +1,9 @@ import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:flutter_hooks_gallery/star_wars/redux.dart'; +import 'package:flutter_hooks_gallery/star_wars/star_wars_api.dart'; import 'package:provider/provider.dart'; -import 'redux.dart'; -import 'star_wars_api.dart'; - /// This handler will take care of async api interactions /// and updating the store afterwards. class _PlanetHandler { @@ -90,10 +89,10 @@ class _PlanetScreenBody extends HookWidget { } class _Error extends StatelessWidget { - const _Error({Key key, this.errorMsg}) : super(key: key); - final String errorMsg; + const _Error({Key key, this.errorMsg}) : super(key: key); + @override Widget build(BuildContext context) { return Column( @@ -102,13 +101,11 @@ class _Error extends StatelessWidget { if (errorMsg != null) Text(errorMsg), RaisedButton( color: Colors.redAccent, + child: const Text('Try again'), onPressed: () async { - await Provider.of<_PlanetHandler>( - context, - listen: false, - ).fetchAndDispatch(); + await Provider.of<_PlanetHandler>(context, listen: false) + .fetchAndDispatch(); }, - child: const Text('Try again'), ), ], ); @@ -116,8 +113,7 @@ class _Error extends StatelessWidget { } class _LoadPageButton extends HookWidget { - const _LoadPageButton({this.next = true}) - : assert(next != null, 'next cannot be null'); + _LoadPageButton({this.next = true}) : assert(next != null); final bool next; @@ -125,12 +121,12 @@ class _LoadPageButton extends HookWidget { Widget build(BuildContext context) { final state = Provider.of(context); return RaisedButton( + child: next ? const Text('Next Page') : const Text('Prev Page'), onPressed: () async { final url = next ? state.planetPage.next : state.planetPage.previous; await Provider.of<_PlanetHandler>(context, listen: false) .fetchAndDispatch(url); }, - child: next ? const Text('Next Page') : const Text('Prev Page'), ); } } @@ -169,9 +165,8 @@ class _PlanetListHeader extends StatelessWidget { return Row( mainAxisAlignment: buttonAlignment, children: [ - if (state.planetPage.previous != null) - const _LoadPageButton(next: false), - if (state.planetPage.next != null) const _LoadPageButton() + if (state.planetPage.previous != null) _LoadPageButton(next: false), + if (state.planetPage.next != null) _LoadPageButton(next: true) ], ); } diff --git a/example/lib/star_wars/redux.dart b/example/lib/star_wars/redux.dart index 110ef94a..e1706def 100644 --- a/example/lib/star_wars/redux.dart +++ b/example/lib/star_wars/redux.dart @@ -1,7 +1,5 @@ import 'package:built_value/built_value.dart'; -import 'package:meta/meta.dart'; - -import 'models.dart'; +import 'package:flutter_hooks_gallery/star_wars/models.dart'; part 'redux.g.dart'; @@ -13,36 +11,44 @@ class FetchPlanetPageActionStart extends ReduxAction {} /// Action that updates state to show that we are loading planets class FetchPlanetPageActionError extends ReduxAction { - FetchPlanetPageActionError(this.errorMsg); - /// Message that should be displayed in the UI final String errorMsg; + + /// constructor + FetchPlanetPageActionError(this.errorMsg); } /// Action to set the planet page class FetchPlanetPageActionSuccess extends ReduxAction { - FetchPlanetPageActionSuccess(this.page); - + /// payload final PlanetPageModel page; + + /// constructor + FetchPlanetPageActionSuccess(this.page); } -@immutable +/// state of the redux store abstract class AppState implements Built { + AppState._(); + + /// default factory factory AppState([void Function(AppStateBuilder) updates]) => _$AppState((u) => u ..isFetchingPlanets = false ..update(updates)); - const AppState._(); - + /// are we currently loading planets bool get isFetchingPlanets; + /// will be set if loading planets failed. This is an error message @nullable String get errorFetchingPlanets; + /// current planet page PlanetPageModel get planetPage; } +/// reducer that is used by useReducer to create the redux store AppState reducer(S state, A action) { final b = state.toBuilder(); if (action is FetchPlanetPageActionStart) { diff --git a/example/lib/star_wars/star_wars_api.dart b/example/lib/star_wars/star_wars_api.dart index a53c60b7..b81a802d 100644 --- a/example/lib/star_wars/star_wars_api.dart +++ b/example/lib/star_wars/star_wars_api.dart @@ -1,17 +1,15 @@ import 'dart:convert'; +import 'package:flutter_hooks_gallery/star_wars/models.dart'; import 'package:http/http.dart' as http; -import 'models.dart'; - /// Api wrapper to retrieve Star Wars related data class StarWarsApi { /// load and return one page of planets - Future getPlanets([String page]) async { + Future getPlanets(String page) async { page ??= 'https://swapi.co/api/planets'; - final response = await http.get(page); - final dynamic json = jsonDecode(utf8.decode(response.bodyBytes)); + dynamic json = jsonDecode(utf8.decode(response.bodyBytes)); return serializers.deserializeWith(PlanetPageModel.serializer, json); } diff --git a/example/lib/use_effect.dart b/example/lib/use_effect.dart index d15c23c0..194d08d8 100644 --- a/example/lib/use_effect.dart +++ b/example/lib/use_effect.dart @@ -15,9 +15,7 @@ class CustomHookExample extends HookWidget { // To update the stored value, `add` data to the StreamController. To get // the latest value from the StreamController, listen to the Stream with // the useStream hook. - // ignore: close_sinks - final StreamController countController = - _useLocalStorageInt('counter'); + StreamController countController = _useLocalStorageInt('counter'); return Scaffold( appBar: AppBar( @@ -29,7 +27,7 @@ class CustomHookExample extends HookWidget { // new value child: HookBuilder( builder: (context) { - final AsyncSnapshot count = useStream(countController.stream); + AsyncSnapshot count = useStream(countController.stream); return !count.hasData ? const CircularProgressIndicator() @@ -78,7 +76,7 @@ StreamController _useLocalStorageInt( useEffect( () { SharedPreferences.getInstance().then((prefs) async { - final int valueFromStorage = prefs.getInt(key); + int valueFromStorage = prefs.getInt(key); controller.add(valueFromStorage ?? defaultValue); }).catchError(controller.addError); return null; diff --git a/example/lib/use_state.dart b/example/lib/use_state.dart index 770fee00..ea62e6df 100644 --- a/example/lib/use_state.dart +++ b/example/lib/use_state.dart @@ -22,11 +22,11 @@ class UseStateExample extends HookWidget { child: Text('Button tapped ${counter.value} times'), ), floatingActionButton: FloatingActionButton( + child: const Icon(Icons.add), // When the button is pressed, update the value of the counter! This // will trigger a rebuild, displaying the latest value in the Text // Widget above! onPressed: () => counter.value++, - child: const Icon(Icons.add), ), ); } diff --git a/example/pubspec.lock b/example/pubspec.lock index b7f532ec..b4385253 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -113,13 +113,6 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.0.2" - clock: - dependency: transitive - description: - name: clock - url: "https://pub.dartlang.org" - source: hosted - version: "1.0.1" code_builder: dependency: transitive description: @@ -162,13 +155,6 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.3.3" - fake_async: - dependency: transitive - description: - name: fake_async - url: "https://pub.dartlang.org" - source: hosted - version: "1.1.0" fixnum: dependency: transitive description: @@ -187,7 +173,7 @@ packages: path: ".." relative: true source: path - version: "0.10.0-dev" + version: "0.9.0" flutter_test: dependency: "direct dev" description: flutter @@ -332,7 +318,7 @@ packages: name: path url: "https://pub.dartlang.org" source: hosted - version: "1.7.0" + version: "1.6.4" pedantic: dependency: transitive description: @@ -456,7 +442,7 @@ packages: name: test_api url: "https://pub.dartlang.org" source: hosted - version: "0.2.16" + version: "0.2.15" timing: dependency: transitive description: diff --git a/lib/src/animation.dart b/lib/src/animation.dart index 8be494b6..3c32126a 100644 --- a/lib/src/animation.dart +++ b/lib/src/animation.dart @@ -46,6 +46,14 @@ AnimationController useAnimationController({ } class _AnimationControllerHook extends Hook { + final Duration duration; + final String debugLabel; + final double initialValue; + final double lowerBound; + final double upperBound; + final TickerProvider vsync; + final AnimationBehavior animationBehavior; + const _AnimationControllerHook({ this.duration, this.debugLabel, @@ -57,14 +65,6 @@ class _AnimationControllerHook extends Hook { List keys, }) : super(keys: keys); - final Duration duration; - final String debugLabel; - final double initialValue; - final double lowerBound; - final double upperBound; - final TickerProvider vsync; - final AnimationBehavior animationBehavior; - @override _AnimationControllerHookState createState() => _AnimationControllerHookState(); @@ -93,7 +93,7 @@ Switching between controller and uncontrolled vsync is not allowed. AnimationController build(BuildContext context) { final vsync = hook.vsync ?? useSingleTickerProvider(keys: hook.keys); - return _animationController ??= AnimationController( + _animationController ??= AnimationController( vsync: vsync, duration: hook.duration, debugLabel: hook.debugLabel, @@ -102,6 +102,8 @@ Switching between controller and uncontrolled vsync is not allowed. animationBehavior: hook.animationBehavior, value: hook.initialValue, ); + + return _animationController; } @override @@ -137,38 +139,33 @@ class _TickerProviderHookState @override Ticker createTicker(TickerCallback onTick) { assert(() { - if (_ticker == null) { - return true; - } + if (_ticker == null) return true; throw FlutterError( '${context.widget.runtimeType} attempted to use a useSingleTickerProvider multiple times.\n' 'A SingleTickerProviderStateMixin can only be used as a TickerProvider once. If a ' 'TickerProvider is used for multiple AnimationController objects, or if it is passed to other ' 'objects and those objects might use it more than one time in total, then instead of ' 'using useSingleTickerProvider, use a regular useTickerProvider.'); - }(), ''); - return _ticker = Ticker(onTick, debugLabel: 'created by $context'); + }()); + _ticker = Ticker(onTick, debugLabel: 'created by $context'); + return _ticker; } @override void dispose() { assert(() { - if (_ticker == null || !_ticker.isActive) { - return true; - } + if (_ticker == null || !_ticker.isActive) return true; throw FlutterError( 'useSingleTickerProvider created a Ticker, but at the time ' 'dispose() was called on the Hook, that Ticker was still active. Tickers used ' ' by AnimationControllers should be disposed by calling dispose() on ' ' the AnimationController itself. Otherwise, the ticker will leak.\n'); - }(), ''); + }()); } @override TickerProvider build(BuildContext context) { - if (_ticker != null) { - _ticker.muted = !TickerMode.of(context); - } + if (_ticker != null) _ticker.muted = !TickerMode.of(context); return this; } } diff --git a/lib/src/async.dart b/lib/src/async.dart index ef3bc4bd..fb1a1a53 100644 --- a/lib/src/async.dart +++ b/lib/src/async.dart @@ -15,12 +15,12 @@ AsyncSnapshot useFuture(Future future, } class _FutureHook extends Hook> { - const _FutureHook(this.future, {this.initialData, this.preserveState = true}); - final Future future; final bool preserveState; final T initialData; + const _FutureHook(this.future, {this.initialData, this.preserveState = true}); + @override _FutureStateHook createState() => _FutureStateHook(); } @@ -66,13 +66,13 @@ class _FutureStateHook extends HookState, _FutureHook> { if (hook.future != null) { final callbackIdentity = Object(); _activeCallbackIdentity = callbackIdentity; - hook.future.then((data) { + hook.future.then((T data) { if (_activeCallbackIdentity == callbackIdentity) { setState(() { _snapshot = AsyncSnapshot.withData(ConnectionState.done, data); }); } - }, onError: (dynamic error) { + }, onError: (Object error) { if (_activeCallbackIdentity == callbackIdentity) { setState(() { _snapshot = AsyncSnapshot.withError(ConnectionState.done, error); @@ -108,12 +108,12 @@ AsyncSnapshot useStream(Stream stream, } class _StreamHook extends Hook> { - const _StreamHook(this.stream, {this.initialData, this.preserveState = true}); - final Stream stream; final T initialData; final bool preserveState; + _StreamHook(this.stream, {this.initialData, this.preserveState = true}); + @override _StreamHookState createState() => _StreamHookState(); } @@ -153,11 +153,11 @@ class _StreamHookState extends HookState, _StreamHook> { void _subscribe() { if (hook.stream != null) { - _subscription = hook.stream.listen((data) { + _subscription = hook.stream.listen((T data) { setState(() { _summary = afterData(_summary, data); }); - }, onError: (dynamic error) { + }, onError: (Object error) { setState(() { _summary = afterError(_summary, error); }); @@ -222,14 +222,14 @@ StreamController useStreamController( } class _StreamControllerHook extends Hook> { - const _StreamControllerHook( - {this.sync = false, this.onListen, this.onCancel, List keys}) - : super(keys: keys); - final bool sync; final VoidCallback onListen; final VoidCallback onCancel; + const _StreamControllerHook( + {this.sync = false, this.onListen, this.onCancel, List keys}) + : super(keys: keys); + @override _StreamControllerHookState createState() => _StreamControllerHookState(); diff --git a/lib/src/framework.dart b/lib/src/framework.dart index 3d68c216..0d68a56b 100644 --- a/lib/src/framework.dart +++ b/lib/src/framework.dart @@ -1,5 +1,3 @@ -import 'dart:collection'; - import 'package:flutter/foundation.dart'; import 'package:flutter/scheduler.dart'; import 'package:flutter/widgets.dart'; @@ -164,9 +162,8 @@ Calling them outside of build method leads to an unstable state and is therefore return false; } - final i1 = p1.iterator; - final i2 = p2.iterator; - // ignore: literal_only_boolean_expressions, returns will abort the loop + var i1 = p1.iterator; + var i2 = p2.iterator; while (true) { if (!i1.moveNext() || !i2.moveNext()) { return true; @@ -196,8 +193,8 @@ Calling them outside of build method leads to an unstable state and is therefore abstract class HookState> { /// Equivalent of [State.context] for [HookState] @protected - BuildContext get context => _element; - HookElement _element; + BuildContext get context => _element.context; + State _element; /// Equivalent of [State.widget] for [HookState] T get hook => _hook; @@ -239,94 +236,35 @@ abstract class HookState> { /// * [State.reassemble] void reassemble() {} - /// Called before a [build] triggered by [markMayNeedRebuild]. - /// - /// If [shouldRebuild] returns `false` on all the hooks that called [markMayNeedRebuild] - /// then this aborts the rebuild of the associated [HookWidget]. - /// - /// There is no guarantee that this method will be called after [markMayNeedRebuild] - /// was called. - /// Some situations where [shouldRebuild] will not be called: - /// - /// - [setState] was called - /// - a previous hook's [shouldRebuild] returned `true` - /// - the associated [HookWidget] changed. - bool shouldRebuild() => true; - - /// Mark the associated [HookWidget] as **potentially** needing to rebuild. - /// - /// As opposed to [setState], the rebuild is optional and can be cancelled right - /// before [HookWidget.build] is called, by having [shouldRebuild] return false. - void markMayNeedRebuild() { - if (_element._isOptionalRebuild != false) { - _element - .._isOptionalRebuild = true - .._shouldRebuildQueue ??= LinkedList() - .._shouldRebuildQueue.add(_Entry(shouldRebuild)) - ..markNeedsBuild(); - } - } - /// Equivalent of [State.setState] for [HookState] @protected void setState(VoidCallback fn) { - fn(); - _element - .._isOptionalRebuild = false - ..markNeedsBuild(); + // ignore: invalid_use_of_protected_member + _element.setState(fn); } } -class _Entry extends LinkedListEntry<_Entry> { - _Entry(this.value); - final T value; -} - /// An [Element] that uses a [HookWidget] as its configuration. class HookElement extends StatefulElement { + static HookElement _currentContext; + /// Creates an element that uses the given widget as its configuration. HookElement(HookWidget widget) : super(widget); - static HookElement _currentContext; Iterator _currentHook; int _hookIndex; List _hooks; - LinkedList<_Entry> _shouldRebuildQueue; - bool _isOptionalRebuild = false; - Widget _buildCache; bool _didFinishBuildOnce = false; bool _debugDidReassemble; bool _debugShouldDispose; bool _debugIsInitHook; - @override - void update(StatefulWidget newWidget) { - _isOptionalRebuild = false; - super.update(newWidget); - } - - @override - void didChangeDependencies() { - _isOptionalRebuild = false; - super.didChangeDependencies(); - } - @override HookWidget get widget => super.widget as HookWidget; @override Widget build() { - final mustRebuild = _isOptionalRebuild != true || - _shouldRebuildQueue.any((cb) => cb.value()); - - _isOptionalRebuild = null; - _shouldRebuildQueue?.clear(); - - if (!mustRebuild) { - return _buildCache; - } - _currentHook = _hooks?.iterator; // first iterator always has null _currentHook?.moveNext(); @@ -336,23 +274,21 @@ class HookElement extends StatefulElement { _debugIsInitHook = false; _debugDidReassemble ??= false; return true; - }(), ''); + }()); HookElement._currentContext = this; - _buildCache = super.build(); + final result = super.build(); HookElement._currentContext = null; // dispose removed items assert(() { - if (!debugHotReloadHooksEnabled) { - return true; - } + if (!debugHotReloadHooksEnabled) return true; if (_debugDidReassemble && _hooks != null) { - while (_hookIndex < _hooks.length) { - _hooks.removeAt(_hookIndex).dispose(); + for (var i = _hookIndex; i < _hooks.length;) { + _hooks.removeAt(i).dispose(); } } return true; - }(), ''); + }()); assert(_hookIndex == (_hooks?.length ?? 0), ''' Build for $widget finished with less hooks used than a previous build. Used $_hookIndex hooks while a previous build had ${_hooks.length}. @@ -360,14 +296,12 @@ This may happen if the call to `Hook.use` is made under some condition. '''); assert(() { - if (!debugHotReloadHooksEnabled) { - return true; - } + if (!debugHotReloadHooksEnabled) return true; _debugDidReassemble = false; return true; - }(), ''); + }()); _didFinishBuildOnce = true; - return _buildCache; + return result; } /// A read-only list of all hooks available. @@ -377,13 +311,9 @@ This may happen if the call to `Hook.use` is made under some condition. List get debugHooks => List.unmodifiable(_hooks); @override - T dependOnInheritedWidgetOfExactType({ - Object aspect, - }) { - assert( - !_debugIsInitHook, - 'Cannot listen to inherited widgets inside HookState.initState. Use HookState.build instead', - ); + T dependOnInheritedWidgetOfExactType( + {Object aspect}) { + assert(!_debugIsInitHook); return super.dependOnInheritedWidgetOfExactType(aspect: aspect); } @@ -459,24 +389,21 @@ This may happen if the call to `Hook.use` is made under some condition. } } return true; - }(), ''); + }()); } R _use(Hook hook) { HookState> hookState; // first build if (_currentHook == null) { - assert(_debugDidReassemble || !_didFinishBuildOnce, - 'No previous hook found at $_hookIndex, is a hook wrapped in a `if`?'); + assert(_debugDidReassemble || !_didFinishBuildOnce); hookState = _createHookState(hook); _hooks ??= []; _hooks.add(hookState); } else { // recreate states on hot-reload of the order changed assert(() { - if (!debugHotReloadHooksEnabled) { - return true; - } + if (!debugHotReloadHooksEnabled) return true; if (!_debugDidReassemble) { return true; } @@ -496,14 +423,13 @@ This may happen if the call to `Hook.use` is made under some condition. hookState = _pushHook(hook); } return true; - }(), ''); + }()); if (!_didFinishBuildOnce && _currentHook.current == null) { hookState = _pushHook(hook); _currentHook.moveNext(); } else { - assert(_currentHook.current != null, - 'No previous hook found at $_hookIndex, is a hook wrapped in a `if`?'); - assert(_debugTypesAreRight(hook), ''); + assert(_currentHook.current != null); + assert(_debugTypesAreRight(hook)); if (_currentHook.current.hook == hook) { hookState = _currentHook.current as HookState>; @@ -528,28 +454,27 @@ This may happen if the call to `Hook.use` is made under some condition. HookState> _replaceHookAt(int index, Hook hook) { _hooks.removeAt(_hookIndex).dispose(); - final hookState = _createHookState(hook); + var hookState = _createHookState(hook); _hooks.insert(_hookIndex, hookState); return hookState; } HookState> _insertHookAt(int index, Hook hook) { - final hookState = _createHookState(hook); + var hookState = _createHookState(hook); _hooks.insert(index, hookState); _resetsIterator(hookState); return hookState; } HookState> _pushHook(Hook hook) { - final hookState = _createHookState(hook); + var hookState = _createHookState(hook); _hooks.add(hookState); _resetsIterator(hookState); return hookState; } bool _debugTypesAreRight(Hook hook) { - assert(_currentHook.current.hook.runtimeType == hook.runtimeType, - 'The previous and new hooks at index $_hookIndex do not match'); + assert(_currentHook.current.hook.runtimeType == hook.runtimeType); return true; } @@ -565,16 +490,16 @@ This may happen if the call to `Hook.use` is made under some condition. assert(() { _debugIsInitHook = true; return true; - }(), ''); + }()); final state = hook.createState() - .._element = this + .._element = this.state .._hook = hook ..initHook(); assert(() { _debugIsInitHook = false; return true; - }(), ''); + }()); return state; } @@ -623,21 +548,21 @@ BuildContext useContext() { /// A [HookWidget] that defer its [HookWidget.build] to a callback class HookBuilder extends HookWidget { + /// The callback used by [HookBuilder] to create a widget. + /// + /// If a [Hook] asks for a rebuild, [builder] will be called again. + /// [builder] must not return `null`. + final Widget Function(BuildContext context) builder; + /// Creates a widget that delegates its build to a callback. /// /// The [builder] argument must not be null. const HookBuilder({ @required this.builder, Key key, - }) : assert(builder != null, '`builder` cannot be null'), + }) : assert(builder != null), super(key: key); - /// The callback used by [HookBuilder] to create a widget. - /// - /// If a [Hook] asks for a rebuild, [builder] will be called again. - /// [builder] must not return `null`. - final Widget Function(BuildContext context) builder; - @override Widget build(BuildContext context) => builder(context); } diff --git a/lib/src/hooks.dart b/lib/src/hooks.dart index 45cb6731..bbec06e9 100644 --- a/lib/src/hooks.dart +++ b/lib/src/hooks.dart @@ -3,8 +3,7 @@ import 'dart:async'; import 'package:flutter/foundation.dart'; import 'package:flutter/scheduler.dart'; import 'package:flutter/widgets.dart'; - -import 'framework.dart'; +import 'package:flutter_hooks/src/framework.dart'; part 'animation.dart'; part 'async.dart'; diff --git a/lib/src/listenable.dart b/lib/src/listenable.dart index f46b419a..b80db47f 100644 --- a/lib/src/listenable.dart +++ b/lib/src/listenable.dart @@ -21,11 +21,10 @@ T useListenable(T listenable) { } class _ListenableHook extends Hook { - const _ListenableHook(this.listenable) - : assert(listenable != null, 'listenable cannot be null'); - final Listenable listenable; + const _ListenableHook(this.listenable) : assert(listenable != null); + @override _ListenableStateHook createState() => _ListenableStateHook(); } @@ -75,11 +74,11 @@ ValueNotifier useValueNotifier([T intialData, List keys]) { } class _ValueNotifierHook extends Hook> { + final T initialData; + const _ValueNotifierHook({List keys, this.initialData}) : super(keys: keys); - final T initialData; - @override _UseValueNotiferHookState createState() => _UseValueNotiferHookState(); } diff --git a/lib/src/misc.dart b/lib/src/misc.dart index c7d6d3ac..5c34a1ae 100644 --- a/lib/src/misc.dart +++ b/lib/src/misc.dart @@ -43,13 +43,13 @@ Store useReducer( } class _ReducerdHook extends Hook> { - const _ReducerdHook(this.reducer, {this.initialState, this.initialAction}) - : assert(reducer != null, 'reducer cannot be null'); - final Reducer reducer; final State initialState; final Action initialAction; + const _ReducerdHook(this.reducer, {this.initialState, this.initialAction}) + : assert(reducer != null); + @override _ReducerdHookState createState() => _ReducerdHookState(); @@ -65,16 +65,17 @@ class _ReducerdHookState void initHook() { super.initHook(); state = hook.reducer(hook.initialState, hook.initialAction); - // TODO support null - assert(state != null, 'reducers cannot return null'); + assert(state != null); } @override void dispatch(Action action) { - final newState = hook.reducer(state, action); - assert(newState != null, 'recuders cannot return null'); - if (state != newState) { - setState(() => state = newState); + final res = hook.reducer(state, action); + assert(res != null); + if (state != res) { + setState(() { + state = res; + }); } } @@ -90,7 +91,7 @@ T usePrevious(T val) { } class _PreviousHook extends Hook { - const _PreviousHook(this.value); + _PreviousHook(this.value); final T value; @@ -120,15 +121,14 @@ void useReassemble(VoidCallback callback) { assert(() { Hook.use(_ReassembleHook(callback)); return true; - }(), ''); + }()); } class _ReassembleHook extends Hook { - const _ReassembleHook(this.callback) - : assert(callback != null, 'callback cannot be null'); - final VoidCallback callback; + _ReassembleHook(this.callback) : assert(callback != null); + @override _ReassembleHookState createState() => _ReassembleHookState(); } diff --git a/lib/src/primitives.dart b/lib/src/primitives.dart index 3264f665..2b1e4b42 100644 --- a/lib/src/primitives.dart +++ b/lib/src/primitives.dart @@ -15,15 +15,14 @@ T useMemoized(T Function() valueBuilder, } class _MemoizedHook extends Hook { - const _MemoizedHook( - this.valueBuilder, { - List keys = const [], - }) : assert(valueBuilder != null, 'valueBuilder cannot be null'), - assert(keys != null, 'keys cannot be null'), - super(keys: keys); - final T Function() valueBuilder; + const _MemoizedHook(this.valueBuilder, + {List keys = const []}) + : assert(valueBuilder != null), + assert(keys != null), + super(keys: keys); + @override _MemoizedHookState createState() => _MemoizedHookState(); } @@ -62,20 +61,17 @@ class _MemoizedHookState extends HookState> { /// controller.forward(); /// }); /// ``` -R useValueChanged( - T value, - R Function(T oldValue, R oldResult) valueChange, -) { +R useValueChanged(T value, R valueChange(T oldValue, R oldResult)) { return Hook.use(_ValueChangedHook(value, valueChange)); } class _ValueChangedHook extends Hook { - const _ValueChangedHook(this.value, this.valueChanged) - : assert(valueChanged != null, 'valueChanged cannot be null'); - final R Function(T oldValue, R oldResult) valueChanged; final T value; + const _ValueChangedHook(this.value, this.valueChanged) + : assert(valueChanged != null); + @override _ValueChangedHookState createState() => _ValueChangedHookState(); } @@ -132,12 +128,12 @@ void useEffect(Dispose Function() effect, [List keys]) { } class _EffectHook extends Hook { + final Dispose Function() effect; + const _EffectHook(this.effect, [List keys]) - : assert(effect != null, 'effect cannot be null'), + : assert(effect != null), super(keys: keys); - final Dispose Function() effect; - @override _EffectHookState createState() => _EffectHookState(); } @@ -211,10 +207,10 @@ ValueNotifier useState([T initialData]) { } class _StateHook extends Hook> { - const _StateHook({this.initialData}); - final T initialData; + const _StateHook({this.initialData}); + @override _StateHookState createState() => _StateHookState(); } diff --git a/lib/src/text_controller.dart b/lib/src/text_controller.dart index 3c6828ba..eb8b33be 100644 --- a/lib/src/text_controller.dart +++ b/lib/src/text_controller.dart @@ -54,20 +54,18 @@ class _TextEditingControllerHookCreator { const useTextEditingController = _TextEditingControllerHookCreator(); class _TextEditingControllerHook extends Hook { - const _TextEditingControllerHook( - this.initialText, - this.initialValue, [ - List keys, - ]) : assert( + final String initialText; + final TextEditingValue initialValue; + + _TextEditingControllerHook(this.initialText, this.initialValue, + [List keys]) + : assert( initialText == null || initialValue == null, "initialText and intialValue can't both be set on a call to " 'useTextEditingController!', ), super(keys: keys); - final String initialText; - final TextEditingValue initialValue; - @override _TextEditingControllerHookState createState() { return _TextEditingControllerHookState(); diff --git a/pubspec.lock b/pubspec.lock new file mode 100644 index 00000000..c1c5dd3b --- /dev/null +++ b/pubspec.lock @@ -0,0 +1,146 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + async: + dependency: transitive + description: + name: async + url: "https://pub.dartlang.org" + source: hosted + version: "2.4.1" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.0" + charcode: + dependency: transitive + description: + name: charcode + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.3" + collection: + dependency: transitive + description: + name: collection + url: "https://pub.dartlang.org" + source: hosted + version: "1.14.12" + flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + matcher: + dependency: transitive + description: + name: matcher + url: "https://pub.dartlang.org" + source: hosted + version: "0.12.6" + meta: + dependency: transitive + description: + name: meta + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.8" + mockito: + dependency: "direct dev" + description: + name: mockito + url: "https://pub.dartlang.org" + source: hosted + version: "4.1.1" + path: + dependency: transitive + description: + name: path + url: "https://pub.dartlang.org" + source: hosted + version: "1.6.4" + pedantic: + dependency: "direct dev" + description: + name: pedantic + url: "https://pub.dartlang.org" + source: hosted + version: "1.9.0" + quiver: + dependency: transitive + description: + name: quiver + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.3" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.99" + source_span: + dependency: transitive + description: + name: source_span + url: "https://pub.dartlang.org" + source: hosted + version: "1.7.0" + stack_trace: + dependency: transitive + description: + name: stack_trace + url: "https://pub.dartlang.org" + source: hosted + version: "1.9.3" + stream_channel: + dependency: transitive + description: + name: stream_channel + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.0" + string_scanner: + dependency: transitive + description: + name: string_scanner + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.5" + term_glyph: + dependency: transitive + description: + name: term_glyph + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.0" + test_api: + dependency: transitive + description: + name: test_api + url: "https://pub.dartlang.org" + source: hosted + version: "0.2.15" + typed_data: + dependency: transitive + description: + name: typed_data + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.6" + vector_math: + dependency: transitive + description: + name: vector_math + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.8" +sdks: + dart: ">=2.7.0 <3.0.0" diff --git a/pubspec.yaml b/pubspec.yaml index a1e7820f..819fa585 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -3,7 +3,7 @@ description: A flutter implementation of React hooks. It adds a new kind of widg homepage: https://github.com/rrousselGit/flutter_hooks author: Remi Rousselet -version: 0.10.0-dev +version: 0.9.0 environment: sdk: ">=2.7.0 <3.0.0" @@ -15,4 +15,5 @@ dependencies: dev_dependencies: flutter_test: sdk: flutter + pedantic: ^1.4.0 mockito: ">=4.0.0 <5.0.0" diff --git a/test/focus_test.dart b/test/focus_test.dart index aec2bf7f..67955652 100644 --- a/test/focus_test.dart +++ b/test/focus_test.dart @@ -17,7 +17,7 @@ void main() { // ignore: invalid_use_of_protected_member expect(focusNode.hasListeners, isFalse); - final previousValue = focusNode; + var previous = focusNode; await tester.pumpWidget( HookBuilder(builder: (_) { @@ -26,7 +26,7 @@ void main() { }), ); - expect(previousValue, focusNode); + expect(previous, focusNode); // ignore: invalid_use_of_protected_member expect(focusNode.hasListeners, isFalse); diff --git a/test/hook_builder_test.dart b/test/hook_builder_test.dart index b722993d..558f54f7 100644 --- a/test/hook_builder_test.dart +++ b/test/hook_builder_test.dart @@ -11,8 +11,7 @@ void main() { return Container(); }); - Widget createBuilder() => HookBuilder(builder: fn.call); - + final createBuilder = () => HookBuilder(builder: fn.call); final _builder = createBuilder(); await tester.pumpWidget(_builder); diff --git a/test/hook_widget_test.dart b/test/hook_widget_test.dart index 71cff077..47fdc5fd 100644 --- a/test/hook_widget_test.dart +++ b/test/hook_widget_test.dart @@ -29,17 +29,15 @@ void main() { final reassemble = Func0(); final builder = Func1(); - HookTest createHook() { - return HookTest( - build: build.call, - dispose: dispose.call, - didUpdateHook: didUpdateHook.call, - reassemble: reassemble.call, - initHook: initHook.call, - didBuild: didBuild, - deactivate: deactivate, - ); - } + final createHook = () => HookTest( + build: build.call, + dispose: dispose.call, + didUpdateHook: didUpdateHook.call, + reassemble: reassemble.call, + initHook: initHook.call, + didBuild: didBuild, + deactivate: deactivate, + ); void verifyNoMoreHookInteration() { verifyNoMoreInteractions(build); @@ -68,102 +66,98 @@ void main() { final state = ValueNotifier(false); final deactivate1 = Func0(); final deactivate2 = Func0(); - - await tester.pumpWidget( - Directionality( - textDirection: TextDirection.rtl, - child: ValueListenableBuilder( + await tester.pumpWidget(Directionality( + textDirection: TextDirection.rtl, + child: ValueListenableBuilder( valueListenable: state, - builder: (context, value, _) { - return Stack(children: [ - Container( - key: const Key('1'), - child: HookBuilder( - key: value ? _key2 : _key1, + builder: (context, bool value, _) => Stack(children: [ + Container( + key: const Key('1'), + child: HookBuilder( + key: value ? _key2 : _key1, + builder: (context) { + Hook.use(HookTest(deactivate: deactivate1)); + return Container(); + }, + ), + ), + HookBuilder( + key: !value ? _key2 : _key1, builder: (context) { - Hook.use(HookTest(deactivate: deactivate1)); + Hook.use(HookTest(deactivate: deactivate2)); return Container(); }, ), - ), - HookBuilder( - key: !value ? _key2 : _key1, - builder: (context) { - Hook.use(HookTest(deactivate: deactivate2)); - return Container(); - }, - ), - ]); - }, - ), - ), - ); - + ])), + )); await tester.pump(); - verifyNever(deactivate1()); verifyNever(deactivate2()); state.value = true; - await tester.pump(); - verifyInOrder([ deactivate1.call(), deactivate2.call(), ]); - await tester.pump(); - verifyNoMoreInteractions(deactivate1); verifyNoMoreInteractions(deactivate2); }); testWidgets('should call other deactivates even if one fails', (tester) async { + final deactivate2 = Func0(); + final _key = GlobalKey(); final onError = Func1(); final oldOnError = FlutterError.onError; FlutterError.onError = onError; - final errorBuilder = ErrorWidget.builder; ErrorWidget.builder = Func1(); when(ErrorWidget.builder(any)).thenReturn(Container()); - - final deactivate = Func0(); - when(deactivate.call()).thenThrow(42); - final deactivate2 = Func0(); - - final _key = GlobalKey(); - - final widget = HookBuilder( - key: _key, - builder: (context) { - Hook.use(HookTest(deactivate: deactivate)); + try { + when(deactivate2.call()).thenThrow(42); + when(builder.call(any)).thenAnswer((invocation) { + Hook.use(createHook()); Hook.use(HookTest(deactivate: deactivate2)); return Container(); - }, - ); - - try { - await tester.pumpWidget(SizedBox(child: widget)); - - verifyNoMoreInteractions(deactivate); - verifyNoMoreInteractions(deactivate2); - - await tester.pumpWidget(widget); + }); + await tester.pumpWidget(Column( + children: [ + Row( + children: [ + HookBuilder( + key: _key, + builder: builder.call, + ) + ], + ), + Container( + child: const SizedBox(), + ), + ], + )); - verifyInOrder([ - deactivate(), - deactivate2(), - ]); + await tester.pumpWidget(Column( + children: [ + Row( + children: [], + ), + Container( + child: HookBuilder( + key: _key, + builder: builder.call, + ), + ), + ], + )); - verify(onError.call(any)).called(1); - verifyNoMoreInteractions(deactivate); - verifyNoMoreInteractions(deactivate2); - } finally { // reset the exception because after the test // flutter tries to deactivate the widget and it causes // and exception - when(deactivate.call()).thenAnswer((_) {}); + when(deactivate2.call()).thenAnswer((_) {}); + verify(onError.call(any)).called((int x) => x > 1); + verify(deactivate.call()).called(1); + } finally { FlutterError.onError = oldOnError; ErrorWidget.builder = errorBuilder; } @@ -182,8 +176,8 @@ void main() { testWidgets('allows using inherited widgets outside of initHook', (tester) async { when(build(any)).thenAnswer((invocation) { - final context = invocation.positionalArguments.first as BuildContext; - context.dependOnInheritedWidgetOfExactType(); + invocation.positionalArguments.first as BuildContext + ..dependOnInheritedWidgetOfExactType(); return null; }); diff --git a/test/memoized_test.dart b/test/memoized_test.dart index e2dc0281..e49d1ce7 100644 --- a/test/memoized_test.dart +++ b/test/memoized_test.dart @@ -16,7 +16,7 @@ void main() { testWidgets('invalid parameters', (tester) async { await tester.pumpWidget(HookBuilder(builder: (context) { - useMemoized(null); + useMemoized(null); return Container(); })); expect(tester.takeException(), isAssertionError); @@ -198,7 +198,7 @@ void main() { }); testWidgets( - "memoized parameter reference do not change don't call valueBuilder", + 'memoized parameter reference do not change don\'t call valueBuilder', (tester) async { int result; final parameters = []; diff --git a/test/mock.dart b/test/mock.dart index a2e449cd..135bf933 100644 --- a/test/mock.dart +++ b/test/mock.dart @@ -1,5 +1,3 @@ -// ignore_for_file: one_member_abstracts - import 'package:flutter/widgets.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -28,7 +26,15 @@ abstract class _Func2 { class Func2 extends Mock implements _Func2 {} class HookTest extends Hook { - // ignore: prefer_const_constructors_in_immutables + final R Function(BuildContext context) build; + final void Function() dispose; + final void Function() didBuild; + final void Function() initHook; + final void Function() deactivate; + final void Function(HookTest previousHook) didUpdateHook; + final void Function() reassemble; + final HookStateTest Function() createStateFn; + HookTest({ this.build, this.dispose, @@ -41,15 +47,6 @@ class HookTest extends Hook { List keys, }) : super(keys: keys); - final R Function(BuildContext context) build; - final void Function() dispose; - final void Function() didBuild; - final void Function() initHook; - final void Function() deactivate; - final void Function(HookTest previousHook) didUpdateHook; - final void Function() reassemble; - final HookStateTest Function() createStateFn; - @override HookStateTest createState() => createStateFn != null ? createStateFn() : HookStateTest(); @@ -126,11 +123,11 @@ Element _rootOf(Element element) { void hotReload(WidgetTester tester) { final root = _rootOf(tester.allElements.first); - TestWidgetsFlutterBinding.ensureInitialized().buildOwner.reassemble(root); + TestWidgetsFlutterBinding.ensureInitialized().buildOwner..reassemble(root); } Future expectPump( - Future Function() pump, + Future pump(), dynamic matcher, { String reason, dynamic skip, diff --git a/test/pre_build_abort_test.dart b/test/pre_build_abort_test.dart deleted file mode 100644 index c076e0dd..00000000 --- a/test/pre_build_abort_test.dart +++ /dev/null @@ -1,291 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter/src/widgets/framework.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:flutter_test/flutter_test.dart'; - -import 'mock.dart'; - -void main() { - testWidgets('shouldRebuild defaults to true', (tester) async { - MayRebuildState first; - var buildCount = 0; - - await tester.pumpWidget( - HookBuilder(builder: (c) { - buildCount++; - first = Hook.use(const MayRebuild()); - - return Container(); - }), - ); - - expect(buildCount, 1); - - first.markMayNeedRebuild(); - await tester.pump(); - - expect(buildCount, 2); - }); - testWidgets('can queue multiple mayRebuilds at once', (tester) async { - final firstSpy = ShouldRebuildMock(); - final secondSpy = ShouldRebuildMock(); - MayRebuildState first; - MayRebuildState second; - var buildCount = 0; - - await tester.pumpWidget( - HookBuilder(builder: (c) { - buildCount++; - first = Hook.use(MayRebuild(firstSpy)); - second = Hook.use(MayRebuild(secondSpy)); - - return Container(); - }), - ); - - verifyNoMoreInteractions(firstSpy); - verifyNoMoreInteractions(secondSpy); - expect(buildCount, 1); - - first.markMayNeedRebuild(); - when(firstSpy()).thenReturn(false); - second.markMayNeedRebuild(); - when(secondSpy()).thenReturn(false); - - await tester.pump(); - - expect(buildCount, 1); - verifyInOrder([ - firstSpy(), - secondSpy(), - ]); - verifyNoMoreInteractions(firstSpy); - verifyNoMoreInteractions(secondSpy); - - first.markMayNeedRebuild(); - when(firstSpy()).thenReturn(true); - second.markMayNeedRebuild(); - when(secondSpy()).thenReturn(false); - - await tester.pump(); - - expect(buildCount, 2); - verify(firstSpy()).called(1); - verifyNoMoreInteractions(firstSpy); - verifyNoMoreInteractions(secondSpy); - }); - testWidgets('pre-build-abort', (tester) async { - var buildCount = 0; - final notifier = ValueNotifier(0); - - await tester.pumpWidget( - HookBuilder(builder: (c) { - buildCount++; - final value = Hook.use(IsPositiveHook(notifier)); - - return Text('$value', textDirection: TextDirection.ltr); - }), - ); - - expect(buildCount, 1); - expect(find.text('true'), findsOneWidget); - expect(find.text('false'), findsNothing); - - notifier.value = -10; - - await tester.pump(); - - expect(buildCount, 2); - expect(find.text('false'), findsOneWidget); - expect(find.text('true'), findsNothing); - - notifier.value = -20; - - await tester.pump(); - - expect(buildCount, 2); - expect(find.text('false'), findsOneWidget); - expect(find.text('true'), findsNothing); - - notifier.value = 20; - - await tester.pump(); - - expect(buildCount, 3); - expect(find.text('true'), findsOneWidget); - expect(find.text('false'), findsNothing); - }); - testWidgets('setState then markMayNeedBuild still force build', - (tester) async { - var buildCount = 0; - final notifier = ValueNotifier(0); - - await tester.pumpWidget( - HookBuilder(builder: (c) { - buildCount++; - useListenable(notifier); - final value = Hook.use(IsPositiveHook(notifier)); - - return Text('$value', textDirection: TextDirection.ltr); - }), - ); - - expect(buildCount, 1); - expect(find.text('true'), findsOneWidget); - expect(find.text('false'), findsNothing); - - notifier.value++; - await tester.pump(); - - expect(buildCount, 2); - expect(find.text('true'), findsOneWidget); - expect(find.text('false'), findsNothing); - }); - testWidgets('markMayNeedBuild then widget rebuild forces build', - (tester) async { - var buildCount = 0; - final notifier = ValueNotifier(0); - - Widget build() { - return HookBuilder(builder: (c) { - buildCount++; - final value = Hook.use(IsPositiveHook(notifier)); - - return Text('$value', textDirection: TextDirection.ltr); - }); - } - - await tester.pumpWidget(build()); - - expect(buildCount, 1); - expect(find.text('true'), findsOneWidget); - expect(find.text('false'), findsNothing); - - notifier.value++; - await tester.pumpWidget(build()); - - expect(buildCount, 2); - expect(find.text('true'), findsOneWidget); - expect(find.text('false'), findsNothing); - }); - testWidgets('markMayNeedBuild then didChangeDepencies forces build', - (tester) async { - var buildCount = 0; - final notifier = ValueNotifier(0); - - final child = HookBuilder(builder: (c) { - buildCount++; - Directionality.of(c); - final value = Hook.use(IsPositiveHook(notifier)); - - return Text('$value', textDirection: TextDirection.ltr); - }); - - await tester.pumpWidget( - Directionality( - textDirection: TextDirection.ltr, - child: child, - ), - ); - - expect(buildCount, 1); - expect(find.text('true'), findsOneWidget); - expect(find.text('false'), findsNothing); - - notifier.value++; - await tester.pumpWidget( - Directionality( - textDirection: TextDirection.rtl, - child: child, - ), - ); - - expect(buildCount, 2); - expect(find.text('true'), findsOneWidget); - expect(find.text('false'), findsNothing); - }); -} - -class IsPositiveHook extends Hook { - const IsPositiveHook(this.notifier); - - final ValueNotifier notifier; - - @override - IsPositiveHookState createState() { - return IsPositiveHookState(); - } -} - -class IsPositiveHookState extends HookState { - bool dirty = true; - bool value; - - @override - void initHook() { - super.initHook(); - value = hook.notifier.value >= 0; - hook.notifier.addListener(listener); - } - - void listener() { - dirty = true; - markMayNeedRebuild(); - } - - @override - bool shouldRebuild() { - if (dirty) { - dirty = false; - final newValue = hook.notifier.value >= 0; - if (newValue != value) { - value = newValue; - return true; - } - } - return false; - } - - @override - bool build(BuildContext context) { - if (dirty) { - dirty = false; - value = hook.notifier.value >= 0; - } - return value; - } - - @override - void dispose() { - hook.notifier.removeListener(listener); - super.dispose(); - } -} - -class MayRebuild extends Hook { - const MayRebuild([this.shouldRebuild]); - - final ShouldRebuildMock shouldRebuild; - - @override - MayRebuildState createState() { - return MayRebuildState(); - } -} - -class MayRebuildState extends HookState { - @override - bool shouldRebuild() { - if (hook.shouldRebuild == null) { - return super.shouldRebuild(); - } - return hook.shouldRebuild(); - } - - @override - MayRebuildState build(BuildContext context) => this; -} - -class ShouldRebuildMock extends Mock { - bool call(); -} diff --git a/test/use_animation_controller_test.dart b/test/use_animation_controller_test.dart index 0f3ddd39..fd078c7c 100644 --- a/test/use_animation_controller_test.dart +++ b/test/use_animation_controller_test.dart @@ -68,7 +68,7 @@ void main() { expect(controller.animationBehavior, AnimationBehavior.preserve); expect(controller.debugLabel, 'Foo'); - final previousController = controller; + var previousController = controller; provider = _TickerProvider(); when(provider.createTicker(any)).thenAnswer((_) { return tester @@ -79,7 +79,11 @@ void main() { HookBuilder(builder: (context) { controller = useAnimationController( vsync: provider, + animationBehavior: AnimationBehavior.normal, duration: const Duration(seconds: 2), + initialValue: 0, + lowerBound: 0, + upperBound: 0, debugLabel: 'Bar', ); return Container(); diff --git a/test/use_animation_test.dart b/test/use_animation_test.dart index 8b037b30..c80a0214 100644 --- a/test/use_animation_test.dart +++ b/test/use_animation_test.dart @@ -19,14 +19,12 @@ void main() { var listenable = AnimationController(vsync: tester); double result; - Future pump() { - return tester.pumpWidget(HookBuilder( - builder: (context) { - result = useAnimation(listenable); - return Container(); - }, - )); - } + pump() => tester.pumpWidget(HookBuilder( + builder: (context) { + result = useAnimation(listenable); + return Container(); + }, + )); await pump(); diff --git a/test/use_effect_test.dart b/test/use_effect_test.dart index d9578601..49d9ef1f 100644 --- a/test/use_effect_test.dart +++ b/test/use_effect_test.dart @@ -3,17 +3,17 @@ import 'package:flutter_hooks/flutter_hooks.dart'; import 'mock.dart'; -void main() { - final effect = Func0(); - final unrelated = Func0(); - List parameters; +final effect = Func0(); +final unrelated = Func0(); +List parameters; - Widget builder() => HookBuilder(builder: (context) { - useEffect(effect.call, parameters); - unrelated.call(); - return Container(); - }); +Widget builder() => HookBuilder(builder: (context) { + useEffect(effect.call, parameters); + unrelated.call(); + return Container(); + }); +void main() { tearDown(() { parameters = null; reset(unrelated); @@ -35,13 +35,11 @@ void main() { final dispose = Func0(); when(effect.call()).thenReturn(dispose.call); - Widget builder() { - return HookBuilder(builder: (context) { - useEffect(effect.call); - unrelated.call(); - return Container(); - }); - } + builder() => HookBuilder(builder: (context) { + useEffect(effect.call); + unrelated.call(); + return Container(); + }); await tester.pumpWidget(builder()); @@ -182,12 +180,10 @@ void main() { final effect = Func0(); List parameters; - Widget builder() { - return HookBuilder(builder: (context) { - useEffect(effect.call, parameters); - return Container(); - }); - } + builder() => HookBuilder(builder: (context) { + useEffect(effect.call, parameters); + return Container(); + }); parameters = ['foo']; final disposerA = Func0(); diff --git a/test/use_future_test.dart b/test/use_future_test.dart index e82f3377..89389e4a 100644 --- a/test/use_future_test.dart +++ b/test/use_future_test.dart @@ -59,7 +59,8 @@ void main() { }; } - testWidgets('gracefully handles transition from null future', (tester) async { + testWidgets('gracefully handles transition from null future', + (WidgetTester tester) async { await tester.pumpWidget(HookBuilder(builder: snapshotText(null))); expect(find.text('AsyncSnapshot(ConnectionState.none, null, null)'), findsOneWidget); @@ -70,7 +71,8 @@ void main() { find.text('AsyncSnapshot(ConnectionState.waiting, null, null)'), findsOneWidget); }); - testWidgets('gracefully handles transition to null future', (tester) async { + testWidgets('gracefully handles transition to null future', + (WidgetTester tester) async { final completer = Completer(); await tester .pumpWidget(HookBuilder(builder: snapshotText(completer.future))); @@ -85,7 +87,8 @@ void main() { expect(find.text('AsyncSnapshot(ConnectionState.none, null, null)'), findsOneWidget); }); - testWidgets('gracefully handles transition to other future', (tester) async { + testWidgets('gracefully handles transition to other future', + (WidgetTester tester) async { final completerA = Completer(); final completerB = Completer(); await tester @@ -104,7 +107,8 @@ void main() { expect(find.text('AsyncSnapshot(ConnectionState.done, B, null)'), findsOneWidget); }); - testWidgets('tracks life-cycle of Future to success', (tester) async { + testWidgets('tracks life-cycle of Future to success', + (WidgetTester tester) async { final completer = Completer(); await tester .pumpWidget(HookBuilder(builder: snapshotText(completer.future))); @@ -117,7 +121,8 @@ void main() { find.text('AsyncSnapshot(ConnectionState.done, hello, null)'), findsOneWidget); }); - testWidgets('tracks life-cycle of Future to error', (tester) async { + testWidgets('tracks life-cycle of Future to error', + (WidgetTester tester) async { final completer = Completer(); await tester .pumpWidget(HookBuilder(builder: snapshotText(completer.future))); @@ -129,7 +134,8 @@ void main() { expect(find.text('AsyncSnapshot(ConnectionState.done, null, bad)'), findsOneWidget); }); - testWidgets('runs the builder using given initial data', (tester) async { + testWidgets('runs the builder using given initial data', + (WidgetTester tester) async { await tester.pumpWidget(HookBuilder( builder: snapshotText( null, @@ -139,7 +145,8 @@ void main() { expect(find.text('AsyncSnapshot(ConnectionState.none, I, null)'), findsOneWidget); }); - testWidgets('ignores initialData when reconfiguring', (tester) async { + testWidgets('ignores initialData when reconfiguring', + (WidgetTester tester) async { await tester.pumpWidget(HookBuilder( builder: snapshotText( null, diff --git a/test/use_listenable_test.dart b/test/use_listenable_test.dart index 928cae1d..6ce5caef 100644 --- a/test/use_listenable_test.dart +++ b/test/use_listenable_test.dart @@ -17,14 +17,12 @@ void main() { testWidgets('useListenable', (tester) async { var listenable = ValueNotifier(0); - Future pump() { - return tester.pumpWidget(HookBuilder( - builder: (context) { - useListenable(listenable); - return Container(); - }, - )); - } + pump() => tester.pumpWidget(HookBuilder( + builder: (context) { + useListenable(listenable); + return Container(); + }, + )); await pump(); diff --git a/test/use_reassemble_test.dart b/test/use_reassemble_test.dart index ab79c91b..2bdd84d4 100644 --- a/test/use_reassemble_test.dart +++ b/test/use_reassemble_test.dart @@ -14,7 +14,7 @@ void main() { ); }); - testWidgets("hot-reload calls useReassemble's callback", (tester) async { + testWidgets('hot-reload calls useReassemble\'s callback', (tester) async { final reassemble = Func0(); await tester.pumpWidget(HookBuilder(builder: (context) { useReassemble(reassemble); diff --git a/test/use_reducer_test.dart b/test/use_reducer_test.dart index a54279ab..78f3b81f 100644 --- a/test/use_reducer_test.dart +++ b/test/use_reducer_test.dart @@ -9,14 +9,12 @@ void main() { final reducer = Func2(); Store store; - Future pump() { - return tester.pumpWidget(HookBuilder( - builder: (context) { - store = useReducer(reducer.call); - return Container(); - }, - )); - } + pump() => tester.pumpWidget(HookBuilder( + builder: (context) { + store = useReducer(reducer.call); + return Container(); + }, + )); when(reducer.call(null, null)).thenReturn(0); await pump(); @@ -98,14 +96,12 @@ void main() { final reducer = Func2(); Store store; - Future pump() { - return tester.pumpWidget(HookBuilder( - builder: (context) { - store = useReducer(reducer.call); - return Container(); - }, - )); - } + pump() => tester.pumpWidget(HookBuilder( + builder: (context) { + store = useReducer(reducer.call); + return Container(); + }, + )); when(reducer.call(null, null)).thenReturn(42); diff --git a/test/use_stream_controller_test.dart b/test/use_stream_controller_test.dart index 782af5c7..943d65ca 100644 --- a/test/use_stream_controller_test.dart +++ b/test/use_stream_controller_test.dart @@ -38,8 +38,8 @@ void main() { expect(() => controller.onResume, throwsUnsupportedError); final previousController = controller; - void onListen() {} - void onCancel() {} + final onListen = () {}; + final onCancel = () {}; await tester.pumpWidget(HookBuilder(builder: (context) { controller = useStreamController( sync: true, @@ -75,8 +75,8 @@ void main() { expect(() => controller.onResume, throwsUnsupportedError); final previousController = controller; - void onListen() {} - void onCancel() {} + final onListen = () {}; + final onCancel = () {}; await tester.pumpWidget(HookBuilder(builder: (context) { controller = useStreamController( onCancel: onCancel, diff --git a/test/use_stream_test.dart b/test/use_stream_test.dart index 2249592c..e48cc827 100644 --- a/test/use_stream_test.dart +++ b/test/use_stream_test.dart @@ -1,5 +1,3 @@ -// ignore_for_file: close_sinks - import 'dart:async'; import 'package:flutter/widgets.dart'; @@ -63,7 +61,8 @@ void main() { }; } - testWidgets('gracefully handles transition from null stream', (tester) async { + testWidgets('gracefully handles transition from null stream', + (WidgetTester tester) async { await tester.pumpWidget(HookBuilder(builder: snapshotText(null))); expect(find.text('AsyncSnapshot(ConnectionState.none, null, null)'), findsOneWidget); @@ -74,7 +73,8 @@ void main() { find.text('AsyncSnapshot(ConnectionState.waiting, null, null)'), findsOneWidget); }); - testWidgets('gracefully handles transition to null stream', (tester) async { + testWidgets('gracefully handles transition to null stream', + (WidgetTester tester) async { final controller = StreamController(); await tester .pumpWidget(HookBuilder(builder: snapshotText(controller.stream))); @@ -85,7 +85,8 @@ void main() { expect(find.text('AsyncSnapshot(ConnectionState.none, null, null)'), findsOneWidget); }); - testWidgets('gracefully handles transition to other stream', (tester) async { + testWidgets('gracefully handles transition to other stream', + (WidgetTester tester) async { final controllerA = StreamController(); final controllerB = StreamController(); await tester @@ -102,7 +103,7 @@ void main() { findsOneWidget); }); testWidgets('tracks events and errors of stream until completion', - (tester) async { + (WidgetTester tester) async { final controller = StreamController(); await tester .pumpWidget(HookBuilder(builder: snapshotText(controller.stream))); @@ -126,7 +127,8 @@ void main() { expect(find.text('AsyncSnapshot(ConnectionState.done, 4, null)'), findsOneWidget); }); - testWidgets('runs the builder using given initial data', (tester) async { + testWidgets('runs the builder using given initial data', + (WidgetTester tester) async { final controller = StreamController(); await tester.pumpWidget(HookBuilder( builder: snapshotText(controller.stream, initialData: 'I'), @@ -134,7 +136,8 @@ void main() { expect(find.text('AsyncSnapshot(ConnectionState.waiting, I, null)'), findsOneWidget); }); - testWidgets('ignores initialData when reconfiguring', (tester) async { + testWidgets('ignores initialData when reconfiguring', + (WidgetTester tester) async { await tester.pumpWidget(HookBuilder( builder: snapshotText(null, initialData: 'I'), )); diff --git a/test/use_text_editing_controller_test.dart b/test/use_text_editing_controller_test.dart index 32833fa8..24a484b9 100644 --- a/test/use_text_editing_controller_test.dart +++ b/test/use_text_editing_controller_test.dart @@ -32,11 +32,9 @@ void main() { // pump another widget so that the old one gets disposed await tester.pumpWidget(Container()); - expect( - () => controller.addListener(null), - throwsA(isFlutterError.having( - (e) => e.message, 'message', contains('disposed'))), - ); + expect(() => controller.addListener(null), throwsA((FlutterError error) { + return error.message.contains('disposed'); + })); }); testWidgets('respects initial text property', (tester) async { diff --git a/test/use_value_changed_test.dart b/test/use_value_changed_test.dart index f99b3872..5bfd4a11 100644 --- a/test/use_value_changed_test.dart +++ b/test/use_value_changed_test.dart @@ -9,7 +9,7 @@ void main() { final _useValueChanged = Func2(); String result; - Future pump() => tester.pumpWidget(HookBuilder( + pump() => tester.pumpWidget(HookBuilder( builder: (context) { result = useValueChanged(value, _useValueChanged.call); return Container(); diff --git a/test/use_value_listenable_test.dart b/test/use_value_listenable_test.dart index e2e3308c..d15011d2 100644 --- a/test/use_value_listenable_test.dart +++ b/test/use_value_listenable_test.dart @@ -18,14 +18,12 @@ void main() { var listenable = ValueNotifier(0); int result; - Future pump() { - return tester.pumpWidget(HookBuilder( - builder: (context) { - result = useValueListenable(listenable); - return Container(); - }, - )); - } + pump() => tester.pumpWidget(HookBuilder( + builder: (context) { + result = useValueListenable(listenable); + return Container(); + }, + )); await pump(); diff --git a/test/use_value_notifier_test.dart b/test/use_value_notifier_test.dart index 4bc660e1..29bba791 100644 --- a/test/use_value_notifier_test.dart +++ b/test/use_value_notifier_test.dart @@ -110,7 +110,8 @@ void main() { expect(state, isNot(previous)); }); - testWidgets("instance stays the same when key don' change", (tester) async { + testWidgets('instance stays the same when key don\' change', + (tester) async { ValueNotifier state; ValueNotifier previous; From 52f6e775cb6c418dca162e9878f32f456d0a9ee3 Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Fri, 12 Jun 2020 16:52:52 +0100 Subject: [PATCH 124/384] Revert "Revert "Pre build abort"" --- .gitignore | 1 + all_lint_rules.yaml | 167 ++++++++++++ analysis_options.yaml | 130 ++++----- example/.flutter-plugins-dependencies | 2 +- example/analysis_options.yaml | 2 + example/lib/custom_hook_function.dart | 4 +- example/lib/main.dart | 13 +- example/lib/star_wars/models.dart | 19 +- example/lib/star_wars/planet_screen.dart | 27 +- example/lib/star_wars/redux.dart | 26 +- example/lib/star_wars/star_wars_api.dart | 8 +- example/lib/use_effect.dart | 8 +- example/lib/use_state.dart | 2 +- example/pubspec.lock | 20 +- lib/src/animation.dart | 39 +-- lib/src/async.dart | 24 +- lib/src/framework.dart | 155 ++++++++--- lib/src/hooks.dart | 3 +- lib/src/listenable.dart | 9 +- lib/src/misc.dart | 28 +- lib/src/primitives.dart | 34 +-- lib/src/text_controller.dart | 14 +- pubspec.lock | 146 ----------- pubspec.yaml | 3 +- test/focus_test.dart | 4 +- test/hook_builder_test.dart | 3 +- test/hook_widget_test.dart | 144 +++++----- test/memoized_test.dart | 4 +- test/mock.dart | 25 +- test/pre_build_abort_test.dart | 291 +++++++++++++++++++++ test/use_animation_controller_test.dart | 6 +- test/use_animation_test.dart | 14 +- test/use_effect_test.dart | 40 +-- test/use_future_test.dart | 21 +- test/use_listenable_test.dart | 14 +- test/use_reassemble_test.dart | 2 +- test/use_reducer_test.dart | 28 +- test/use_stream_controller_test.dart | 8 +- test/use_stream_test.dart | 19 +- test/use_text_editing_controller_test.dart | 8 +- test/use_value_changed_test.dart | 2 +- test/use_value_listenable_test.dart | 14 +- test/use_value_notifier_test.dart | 3 +- 43 files changed, 975 insertions(+), 559 deletions(-) create mode 100644 all_lint_rules.yaml delete mode 100644 pubspec.lock create mode 100644 test/pre_build_abort_test.dart diff --git a/.gitignore b/.gitignore index 0e13fdfe..884c2afd 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,4 @@ build/ android/ ios/ /coverage +.pubspec.lock \ No newline at end of file diff --git a/all_lint_rules.yaml b/all_lint_rules.yaml new file mode 100644 index 00000000..238664b5 --- /dev/null +++ b/all_lint_rules.yaml @@ -0,0 +1,167 @@ +linter: + rules: + - always_declare_return_types + - always_put_control_body_on_new_line + - always_put_required_named_parameters_first + - always_require_non_null_named_parameters + - always_specify_types + - annotate_overrides + - avoid_annotating_with_dynamic + - avoid_as + - avoid_bool_literals_in_conditional_expressions + - avoid_catches_without_on_clauses + - avoid_catching_errors + - avoid_classes_with_only_static_members + - avoid_double_and_int_checks + - avoid_empty_else + - avoid_equals_and_hash_code_on_mutable_classes + - avoid_escaping_inner_quotes + - avoid_field_initializers_in_const_classes + - avoid_function_literals_in_foreach_calls + - avoid_implementing_value_types + - avoid_init_to_null + - avoid_js_rounded_ints + - avoid_null_checks_in_equality_operators + - avoid_positional_boolean_parameters + - avoid_print + - avoid_private_typedef_functions + - avoid_redundant_argument_values + - avoid_relative_lib_imports + - avoid_renaming_method_parameters + - avoid_return_types_on_setters + - avoid_returning_null + - avoid_returning_null_for_future + - avoid_returning_null_for_void + - avoid_returning_this + - avoid_setters_without_getters + - avoid_shadowing_type_parameters + - avoid_single_cascade_in_expression_statements + - avoid_slow_async_io + - avoid_types_as_parameter_names + - avoid_types_on_closure_parameters + - avoid_unnecessary_containers + - avoid_unused_constructor_parameters + - avoid_void_async + - avoid_web_libraries_in_flutter + - await_only_futures + - camel_case_extensions + - camel_case_types + - cancel_subscriptions + - cascade_invocations + - close_sinks + - comment_references + - constant_identifier_names + - control_flow_in_finally + - curly_braces_in_flow_control_structures + - diagnostic_describe_all_properties + - directives_ordering + - empty_catches + - empty_constructor_bodies + - empty_statements + - file_names + - flutter_style_todos + - hash_and_equals + - implementation_imports + - invariant_booleans + - iterable_contains_unrelated_type + - join_return_with_assignment + - leading_newlines_in_multiline_strings + - library_names + - library_prefixes + - lines_longer_than_80_chars + - list_remove_unrelated_type + - literal_only_boolean_expressions + - missing_whitespace_between_adjacent_strings + - no_adjacent_strings_in_list + - no_duplicate_case_values + - no_logic_in_create_state + - no_runtimeType_toString + - non_constant_identifier_names + - null_closures + - omit_local_variable_types + - one_member_abstracts + - only_throw_errors + - overridden_fields + - package_api_docs + - package_names + - package_prefixed_library_names + - parameter_assignments + - prefer_adjacent_string_concatenation + - prefer_asserts_in_initializer_lists + - prefer_asserts_with_message + - prefer_collection_literals + - prefer_conditional_assignment + - prefer_const_constructors + - prefer_const_constructors_in_immutables + - prefer_const_declarations + - prefer_const_literals_to_create_immutables + - prefer_constructors_over_static_methods + - prefer_contains + - prefer_double_quotes + - prefer_equal_for_default_values + - prefer_expression_function_bodies + - prefer_final_fields + - prefer_final_in_for_each + - prefer_final_locals + - prefer_for_elements_to_map_fromIterable + - prefer_foreach + - prefer_function_declarations_over_variables + - prefer_generic_function_type_aliases + - prefer_if_elements_to_conditional_expressions + - prefer_if_null_operators + - prefer_initializing_formals + - prefer_inlined_adds + - prefer_int_literals + - prefer_interpolation_to_compose_strings + - prefer_is_empty + - prefer_is_not_empty + - prefer_is_not_operator + - prefer_iterable_whereType + - prefer_mixin + - prefer_null_aware_operators + - prefer_relative_imports + - prefer_single_quotes + - prefer_spread_collections + - prefer_typing_uninitialized_variables + - prefer_void_to_null + - provide_deprecation_message + - public_member_api_docs + - recursive_getters + - slash_for_doc_comments + - sort_child_properties_last + - sort_constructors_first + - sort_pub_dependencies + - sort_unnamed_constructors_first + - test_types_in_equals + - throw_in_finally + - type_annotate_public_apis + - type_init_formals + - unawaited_futures + - unnecessary_await_in_return + - unnecessary_brace_in_string_interps + - unnecessary_const + - unnecessary_final + - unnecessary_getters_setters + - unnecessary_lambdas + - unnecessary_new + - unnecessary_null_aware_assignments + - unnecessary_null_in_if_null_operators + - unnecessary_overrides + - unnecessary_parenthesis + - unnecessary_raw_strings + - unnecessary_statements + - unnecessary_string_escapes + - unnecessary_string_interpolations + - unnecessary_this + - unrelated_type_equality_checks + - unsafe_html + - use_full_hex_values_for_flutter_colors + - use_function_type_syntax_for_parameters + - use_key_in_widget_constructors + - use_raw_strings + - use_rethrow_when_possible + - use_setters_to_change_properties + - use_string_buffers + - use_to_and_as_if_applicable + - valid_regexps + - void_checks \ No newline at end of file diff --git a/analysis_options.yaml b/analysis_options.yaml index cd7a9506..dc5b2590 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -1,80 +1,64 @@ -include: package:pedantic/analysis_options.yaml +include: all_lint_rules.yaml analyzer: + exclude: + - "**/*.g.dart" + - "**/*.freezed.dart" strong-mode: implicit-casts: false implicit-dynamic: false errors: - todo: error - include_file_not_found: ignore + # Otherwise cause the import of all_lint_rules to warn because of some rules conflicts. + # We explicitly enabled even conflicting rules and are fixing the conflict + # in this file + included_file_warning: ignore + + # Causes false positives (https://github.com/dart-lang/sdk/issues/41571 + top_level_function_literal_block: ignore + linter: rules: - - public_member_api_docs - - annotate_overrides - - avoid_empty_else - - avoid_function_literals_in_foreach_calls - - avoid_init_to_null - - avoid_null_checks_in_equality_operators - - avoid_relative_lib_imports - - avoid_renaming_method_parameters - - avoid_return_types_on_setters - - avoid_returning_null - - avoid_types_as_parameter_names - - avoid_unused_constructor_parameters - - await_only_futures - - camel_case_types - - cancel_subscriptions - - cascade_invocations - - comment_references - - constant_identifier_names - - control_flow_in_finally - - directives_ordering - - empty_catches - - empty_constructor_bodies - - empty_statements - - hash_and_equals - - implementation_imports - - invariant_booleans - - iterable_contains_unrelated_type - - library_names - - library_prefixes - - list_remove_unrelated_type - - no_adjacent_strings_in_list - - no_duplicate_case_values - - non_constant_identifier_names - - null_closures - - omit_local_variable_types - - only_throw_errors - - overridden_fields - - package_api_docs - - package_names - - package_prefixed_library_names - - prefer_adjacent_string_concatenation - - prefer_collection_literals - - prefer_conditional_assignment - - prefer_const_constructors - - prefer_contains - - prefer_equal_for_default_values - - prefer_final_fields - - prefer_initializing_formals - - prefer_interpolation_to_compose_strings - - prefer_is_empty - - prefer_is_not_empty - - prefer_single_quotes - - prefer_typing_uninitialized_variables - - recursive_getters - - slash_for_doc_comments - - test_types_in_equals - - throw_in_finally - - type_init_formals - - unawaited_futures - - unnecessary_brace_in_string_interps - - unnecessary_const - - unnecessary_getters_setters - - unnecessary_lambdas - - unnecessary_new - - unnecessary_null_aware_assignments - - unnecessary_statements - - unnecessary_this - - unrelated_type_equality_checks - - use_rethrow_when_possible - - valid_regexps + # Personal preference. I don't find it more readable + cascade_invocations: false + + # Conflicts with `prefer_single_quotes` + # Single quotes are easier to type and don't compromise on readability. + prefer_double_quotes: false + + # Conflicts with `omit_local_variable_types` and other rules. + # As per Dart guidelines, we want to avoid unnecessary types to make the code + # more readable. + # See https://dart.dev/guides/language/effective-dart/design#avoid-type-annotating-initialized-local-variables + always_specify_types: false + + # Incompatible with `prefer_final_locals` + # Having immutable local variables makes larger functions more predictible + # so we will use `prefer_final_locals` instead. + unnecessary_final: false + + # Not quite suitable for Flutter, which may have a `build` method with a single + # return, but that return is still complex enough that a "body" is worth it. + prefer_expression_function_bodies: false + + # Conflicts with the convention used by flutter, which puts `Key key` + # and `@required Widget child` last. + always_put_required_named_parameters_first: false + + # `as` is not that bad (especially with the upcoming non-nullable types). + # Explicit exceptions is better than implicit exceptions. + avoid_as: false + + # This project doesn't use Flutter-style todos + flutter_style_todos: false + + # There are situations where we voluntarily want to catch everything, + # especially as a library. + avoid_catches_without_on_clauses: false + + # Boring as it sometimes force a line of 81 characters to be split in two. + # As long as we try to respect that 80 characters limit, going slightly + # above is fine. + lines_longer_than_80_chars: false + + # Conflicts with disabling `implicit-dynamic` + avoid_annotating_with_dynamic: false + diff --git a/example/.flutter-plugins-dependencies b/example/.flutter-plugins-dependencies index 04a64a12..1cdfcc0d 100644 --- a/example/.flutter-plugins-dependencies +++ b/example/.flutter-plugins-dependencies @@ -1 +1 @@ -{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"shared_preferences","path":"/Users/remiguest/.pub-cache/hosted/pub.dartlang.org/shared_preferences-0.4.3/","dependencies":[]}],"android":[{"name":"shared_preferences","path":"/Users/remiguest/.pub-cache/hosted/pub.dartlang.org/shared_preferences-0.4.3/","dependencies":[]}],"macos":[],"linux":[],"windows":[],"web":[]},"dependencyGraph":[{"name":"shared_preferences","dependencies":[]}],"date_created":"2020-04-11 11:04:13.397414","version":"1.18.0-5.0.pre.57"} \ No newline at end of file +{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"shared_preferences","path":"/Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/shared_preferences-0.4.3/","dependencies":[]}],"android":[{"name":"shared_preferences","path":"/Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/shared_preferences-0.4.3/","dependencies":[]}],"macos":[],"linux":[],"windows":[],"web":[]},"dependencyGraph":[{"name":"shared_preferences","dependencies":[]}],"date_created":"2020-06-12 09:30:06.628061","version":"1.19.0-4.0.pre.128"} \ No newline at end of file diff --git a/example/analysis_options.yaml b/example/analysis_options.yaml index b6d5666f..f52cc3f8 100644 --- a/example/analysis_options.yaml +++ b/example/analysis_options.yaml @@ -9,3 +9,5 @@ analyzer: linter: rules: public_member_api_docs: false + avoid_print: false + use_key_in_widget_constructors: false diff --git a/example/lib/custom_hook_function.dart b/example/lib/custom_hook_function.dart index cbdfb7d0..168de53c 100644 --- a/example/lib/custom_hook_function.dart +++ b/example/lib/custom_hook_function.dart @@ -21,11 +21,11 @@ class CustomHookFunctionExample extends HookWidget { child: Text('Button tapped ${counter.value} times'), ), floatingActionButton: FloatingActionButton( - child: const Icon(Icons.add), // When the button is pressed, update the value of the counter! This // will trigger a rebuild as well as printing the latest value to the // console! onPressed: () => counter.value++, + child: const Icon(Icons.add), ), ); } @@ -39,7 +39,7 @@ ValueNotifier useLoggedState([T initialData]) { final result = useState(initialData); // Next, call the useValueChanged hook to print the state whenever it changes - useValueChanged(result.value, (T _, T __) { + useValueChanged(result.value, (_, __) { print(result.value); }); diff --git a/example/lib/main.dart b/example/lib/main.dart index 96f2385c..9570ada7 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -1,9 +1,10 @@ // ignore_for_file: omit_local_variable_types import 'package:flutter/material.dart'; -import 'package:flutter_hooks_gallery/star_wars/planet_screen.dart'; -import 'package:flutter_hooks_gallery/use_effect.dart'; -import 'package:flutter_hooks_gallery/use_state.dart'; -import 'package:flutter_hooks_gallery/use_stream.dart'; + +import 'star_wars/planet_screen.dart'; +import 'use_effect.dart'; +import 'use_state.dart'; +import 'use_stream.dart'; void main() => runApp(HooksGalleryApp()); @@ -43,11 +44,11 @@ class HooksGalleryApp extends StatelessWidget { } class _GalleryItem extends StatelessWidget { + const _GalleryItem({this.title, this.builder}); + final String title; final WidgetBuilder builder; - const _GalleryItem({this.title, this.builder}); - @override Widget build(BuildContext context) { return ListTile( diff --git a/example/lib/star_wars/models.dart b/example/lib/star_wars/models.dart index 6cb32683..fe2370ff 100644 --- a/example/lib/star_wars/models.dart +++ b/example/lib/star_wars/models.dart @@ -4,6 +4,7 @@ import 'package:built_collection/built_collection.dart'; import 'package:built_value/built_value.dart'; import 'package:built_value/serializer.dart'; import 'package:built_value/standard_json_plugin.dart'; +import 'package:meta/meta.dart'; part 'models.g.dart'; @@ -15,13 +16,14 @@ part 'models.g.dart'; final Serializers serializers = (_$serializers.toBuilder()..addPlugin(StandardJsonPlugin())).build(); -/// equals one page +@immutable abstract class PlanetPageModel implements Built { - PlanetPageModel._(); + factory PlanetPageModel([ + void Function(PlanetPageModelBuilder) updates, + ]) = _$PlanetPageModel; - factory PlanetPageModel([void Function(PlanetPageModelBuilder) updates]) = - _$PlanetPageModel; + const PlanetPageModel._(); static Serializer get serializer => _$planetPageModelSerializer; @@ -35,12 +37,13 @@ abstract class PlanetPageModel BuiltList get results; } -/// equals one planet +@immutable abstract class PlanetModel implements Built { - PlanetModel._(); + factory PlanetModel([ + void Function(PlanetModelBuilder) updates, + ]) = _$PlanetModel; - factory PlanetModel([void Function(PlanetModelBuilder) updates]) = - _$PlanetModel; + const PlanetModel._(); static Serializer get serializer => _$planetModelSerializer; diff --git a/example/lib/star_wars/planet_screen.dart b/example/lib/star_wars/planet_screen.dart index 276dd36a..959886a5 100644 --- a/example/lib/star_wars/planet_screen.dart +++ b/example/lib/star_wars/planet_screen.dart @@ -1,9 +1,10 @@ import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:flutter_hooks_gallery/star_wars/redux.dart'; -import 'package:flutter_hooks_gallery/star_wars/star_wars_api.dart'; import 'package:provider/provider.dart'; +import 'redux.dart'; +import 'star_wars_api.dart'; + /// This handler will take care of async api interactions /// and updating the store afterwards. class _PlanetHandler { @@ -89,10 +90,10 @@ class _PlanetScreenBody extends HookWidget { } class _Error extends StatelessWidget { - final String errorMsg; - const _Error({Key key, this.errorMsg}) : super(key: key); + final String errorMsg; + @override Widget build(BuildContext context) { return Column( @@ -101,11 +102,13 @@ class _Error extends StatelessWidget { if (errorMsg != null) Text(errorMsg), RaisedButton( color: Colors.redAccent, - child: const Text('Try again'), onPressed: () async { - await Provider.of<_PlanetHandler>(context, listen: false) - .fetchAndDispatch(); + await Provider.of<_PlanetHandler>( + context, + listen: false, + ).fetchAndDispatch(); }, + child: const Text('Try again'), ), ], ); @@ -113,7 +116,8 @@ class _Error extends StatelessWidget { } class _LoadPageButton extends HookWidget { - _LoadPageButton({this.next = true}) : assert(next != null); + const _LoadPageButton({this.next = true}) + : assert(next != null, 'next cannot be null'); final bool next; @@ -121,12 +125,12 @@ class _LoadPageButton extends HookWidget { Widget build(BuildContext context) { final state = Provider.of(context); return RaisedButton( - child: next ? const Text('Next Page') : const Text('Prev Page'), onPressed: () async { final url = next ? state.planetPage.next : state.planetPage.previous; await Provider.of<_PlanetHandler>(context, listen: false) .fetchAndDispatch(url); }, + child: next ? const Text('Next Page') : const Text('Prev Page'), ); } } @@ -165,8 +169,9 @@ class _PlanetListHeader extends StatelessWidget { return Row( mainAxisAlignment: buttonAlignment, children: [ - if (state.planetPage.previous != null) _LoadPageButton(next: false), - if (state.planetPage.next != null) _LoadPageButton(next: true) + if (state.planetPage.previous != null) + const _LoadPageButton(next: false), + if (state.planetPage.next != null) const _LoadPageButton() ], ); } diff --git a/example/lib/star_wars/redux.dart b/example/lib/star_wars/redux.dart index e1706def..110ef94a 100644 --- a/example/lib/star_wars/redux.dart +++ b/example/lib/star_wars/redux.dart @@ -1,5 +1,7 @@ import 'package:built_value/built_value.dart'; -import 'package:flutter_hooks_gallery/star_wars/models.dart'; +import 'package:meta/meta.dart'; + +import 'models.dart'; part 'redux.g.dart'; @@ -11,44 +13,36 @@ class FetchPlanetPageActionStart extends ReduxAction {} /// Action that updates state to show that we are loading planets class FetchPlanetPageActionError extends ReduxAction { + FetchPlanetPageActionError(this.errorMsg); + /// Message that should be displayed in the UI final String errorMsg; - - /// constructor - FetchPlanetPageActionError(this.errorMsg); } /// Action to set the planet page class FetchPlanetPageActionSuccess extends ReduxAction { - /// payload - final PlanetPageModel page; - - /// constructor FetchPlanetPageActionSuccess(this.page); + + final PlanetPageModel page; } -/// state of the redux store +@immutable abstract class AppState implements Built { - AppState._(); - - /// default factory factory AppState([void Function(AppStateBuilder) updates]) => _$AppState((u) => u ..isFetchingPlanets = false ..update(updates)); - /// are we currently loading planets + const AppState._(); + bool get isFetchingPlanets; - /// will be set if loading planets failed. This is an error message @nullable String get errorFetchingPlanets; - /// current planet page PlanetPageModel get planetPage; } -/// reducer that is used by useReducer to create the redux store AppState reducer(S state, A action) { final b = state.toBuilder(); if (action is FetchPlanetPageActionStart) { diff --git a/example/lib/star_wars/star_wars_api.dart b/example/lib/star_wars/star_wars_api.dart index b81a802d..a53c60b7 100644 --- a/example/lib/star_wars/star_wars_api.dart +++ b/example/lib/star_wars/star_wars_api.dart @@ -1,15 +1,17 @@ import 'dart:convert'; -import 'package:flutter_hooks_gallery/star_wars/models.dart'; import 'package:http/http.dart' as http; +import 'models.dart'; + /// Api wrapper to retrieve Star Wars related data class StarWarsApi { /// load and return one page of planets - Future getPlanets(String page) async { + Future getPlanets([String page]) async { page ??= 'https://swapi.co/api/planets'; + final response = await http.get(page); - dynamic json = jsonDecode(utf8.decode(response.bodyBytes)); + final dynamic json = jsonDecode(utf8.decode(response.bodyBytes)); return serializers.deserializeWith(PlanetPageModel.serializer, json); } diff --git a/example/lib/use_effect.dart b/example/lib/use_effect.dart index 194d08d8..d15c23c0 100644 --- a/example/lib/use_effect.dart +++ b/example/lib/use_effect.dart @@ -15,7 +15,9 @@ class CustomHookExample extends HookWidget { // To update the stored value, `add` data to the StreamController. To get // the latest value from the StreamController, listen to the Stream with // the useStream hook. - StreamController countController = _useLocalStorageInt('counter'); + // ignore: close_sinks + final StreamController countController = + _useLocalStorageInt('counter'); return Scaffold( appBar: AppBar( @@ -27,7 +29,7 @@ class CustomHookExample extends HookWidget { // new value child: HookBuilder( builder: (context) { - AsyncSnapshot count = useStream(countController.stream); + final AsyncSnapshot count = useStream(countController.stream); return !count.hasData ? const CircularProgressIndicator() @@ -76,7 +78,7 @@ StreamController _useLocalStorageInt( useEffect( () { SharedPreferences.getInstance().then((prefs) async { - int valueFromStorage = prefs.getInt(key); + final int valueFromStorage = prefs.getInt(key); controller.add(valueFromStorage ?? defaultValue); }).catchError(controller.addError); return null; diff --git a/example/lib/use_state.dart b/example/lib/use_state.dart index ea62e6df..770fee00 100644 --- a/example/lib/use_state.dart +++ b/example/lib/use_state.dart @@ -22,11 +22,11 @@ class UseStateExample extends HookWidget { child: Text('Button tapped ${counter.value} times'), ), floatingActionButton: FloatingActionButton( - child: const Icon(Icons.add), // When the button is pressed, update the value of the counter! This // will trigger a rebuild, displaying the latest value in the Text // Widget above! onPressed: () => counter.value++, + child: const Icon(Icons.add), ), ); } diff --git a/example/pubspec.lock b/example/pubspec.lock index b4385253..b7f532ec 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -113,6 +113,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.0.2" + clock: + dependency: transitive + description: + name: clock + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.1" code_builder: dependency: transitive description: @@ -155,6 +162,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.3.3" + fake_async: + dependency: transitive + description: + name: fake_async + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.0" fixnum: dependency: transitive description: @@ -173,7 +187,7 @@ packages: path: ".." relative: true source: path - version: "0.9.0" + version: "0.10.0-dev" flutter_test: dependency: "direct dev" description: flutter @@ -318,7 +332,7 @@ packages: name: path url: "https://pub.dartlang.org" source: hosted - version: "1.6.4" + version: "1.7.0" pedantic: dependency: transitive description: @@ -442,7 +456,7 @@ packages: name: test_api url: "https://pub.dartlang.org" source: hosted - version: "0.2.15" + version: "0.2.16" timing: dependency: transitive description: diff --git a/lib/src/animation.dart b/lib/src/animation.dart index 3c32126a..8be494b6 100644 --- a/lib/src/animation.dart +++ b/lib/src/animation.dart @@ -46,14 +46,6 @@ AnimationController useAnimationController({ } class _AnimationControllerHook extends Hook { - final Duration duration; - final String debugLabel; - final double initialValue; - final double lowerBound; - final double upperBound; - final TickerProvider vsync; - final AnimationBehavior animationBehavior; - const _AnimationControllerHook({ this.duration, this.debugLabel, @@ -65,6 +57,14 @@ class _AnimationControllerHook extends Hook { List keys, }) : super(keys: keys); + final Duration duration; + final String debugLabel; + final double initialValue; + final double lowerBound; + final double upperBound; + final TickerProvider vsync; + final AnimationBehavior animationBehavior; + @override _AnimationControllerHookState createState() => _AnimationControllerHookState(); @@ -93,7 +93,7 @@ Switching between controller and uncontrolled vsync is not allowed. AnimationController build(BuildContext context) { final vsync = hook.vsync ?? useSingleTickerProvider(keys: hook.keys); - _animationController ??= AnimationController( + return _animationController ??= AnimationController( vsync: vsync, duration: hook.duration, debugLabel: hook.debugLabel, @@ -102,8 +102,6 @@ Switching between controller and uncontrolled vsync is not allowed. animationBehavior: hook.animationBehavior, value: hook.initialValue, ); - - return _animationController; } @override @@ -139,33 +137,38 @@ class _TickerProviderHookState @override Ticker createTicker(TickerCallback onTick) { assert(() { - if (_ticker == null) return true; + if (_ticker == null) { + return true; + } throw FlutterError( '${context.widget.runtimeType} attempted to use a useSingleTickerProvider multiple times.\n' 'A SingleTickerProviderStateMixin can only be used as a TickerProvider once. If a ' 'TickerProvider is used for multiple AnimationController objects, or if it is passed to other ' 'objects and those objects might use it more than one time in total, then instead of ' 'using useSingleTickerProvider, use a regular useTickerProvider.'); - }()); - _ticker = Ticker(onTick, debugLabel: 'created by $context'); - return _ticker; + }(), ''); + return _ticker = Ticker(onTick, debugLabel: 'created by $context'); } @override void dispose() { assert(() { - if (_ticker == null || !_ticker.isActive) return true; + if (_ticker == null || !_ticker.isActive) { + return true; + } throw FlutterError( 'useSingleTickerProvider created a Ticker, but at the time ' 'dispose() was called on the Hook, that Ticker was still active. Tickers used ' ' by AnimationControllers should be disposed by calling dispose() on ' ' the AnimationController itself. Otherwise, the ticker will leak.\n'); - }()); + }(), ''); } @override TickerProvider build(BuildContext context) { - if (_ticker != null) _ticker.muted = !TickerMode.of(context); + if (_ticker != null) { + _ticker.muted = !TickerMode.of(context); + } return this; } } diff --git a/lib/src/async.dart b/lib/src/async.dart index fb1a1a53..ef3bc4bd 100644 --- a/lib/src/async.dart +++ b/lib/src/async.dart @@ -15,12 +15,12 @@ AsyncSnapshot useFuture(Future future, } class _FutureHook extends Hook> { + const _FutureHook(this.future, {this.initialData, this.preserveState = true}); + final Future future; final bool preserveState; final T initialData; - const _FutureHook(this.future, {this.initialData, this.preserveState = true}); - @override _FutureStateHook createState() => _FutureStateHook(); } @@ -66,13 +66,13 @@ class _FutureStateHook extends HookState, _FutureHook> { if (hook.future != null) { final callbackIdentity = Object(); _activeCallbackIdentity = callbackIdentity; - hook.future.then((T data) { + hook.future.then((data) { if (_activeCallbackIdentity == callbackIdentity) { setState(() { _snapshot = AsyncSnapshot.withData(ConnectionState.done, data); }); } - }, onError: (Object error) { + }, onError: (dynamic error) { if (_activeCallbackIdentity == callbackIdentity) { setState(() { _snapshot = AsyncSnapshot.withError(ConnectionState.done, error); @@ -108,12 +108,12 @@ AsyncSnapshot useStream(Stream stream, } class _StreamHook extends Hook> { + const _StreamHook(this.stream, {this.initialData, this.preserveState = true}); + final Stream stream; final T initialData; final bool preserveState; - _StreamHook(this.stream, {this.initialData, this.preserveState = true}); - @override _StreamHookState createState() => _StreamHookState(); } @@ -153,11 +153,11 @@ class _StreamHookState extends HookState, _StreamHook> { void _subscribe() { if (hook.stream != null) { - _subscription = hook.stream.listen((T data) { + _subscription = hook.stream.listen((data) { setState(() { _summary = afterData(_summary, data); }); - }, onError: (Object error) { + }, onError: (dynamic error) { setState(() { _summary = afterError(_summary, error); }); @@ -222,14 +222,14 @@ StreamController useStreamController( } class _StreamControllerHook extends Hook> { - final bool sync; - final VoidCallback onListen; - final VoidCallback onCancel; - const _StreamControllerHook( {this.sync = false, this.onListen, this.onCancel, List keys}) : super(keys: keys); + final bool sync; + final VoidCallback onListen; + final VoidCallback onCancel; + @override _StreamControllerHookState createState() => _StreamControllerHookState(); diff --git a/lib/src/framework.dart b/lib/src/framework.dart index 0d68a56b..3d68c216 100644 --- a/lib/src/framework.dart +++ b/lib/src/framework.dart @@ -1,3 +1,5 @@ +import 'dart:collection'; + import 'package:flutter/foundation.dart'; import 'package:flutter/scheduler.dart'; import 'package:flutter/widgets.dart'; @@ -162,8 +164,9 @@ Calling them outside of build method leads to an unstable state and is therefore return false; } - var i1 = p1.iterator; - var i2 = p2.iterator; + final i1 = p1.iterator; + final i2 = p2.iterator; + // ignore: literal_only_boolean_expressions, returns will abort the loop while (true) { if (!i1.moveNext() || !i2.moveNext()) { return true; @@ -193,8 +196,8 @@ Calling them outside of build method leads to an unstable state and is therefore abstract class HookState> { /// Equivalent of [State.context] for [HookState] @protected - BuildContext get context => _element.context; - State _element; + BuildContext get context => _element; + HookElement _element; /// Equivalent of [State.widget] for [HookState] T get hook => _hook; @@ -236,35 +239,94 @@ abstract class HookState> { /// * [State.reassemble] void reassemble() {} + /// Called before a [build] triggered by [markMayNeedRebuild]. + /// + /// If [shouldRebuild] returns `false` on all the hooks that called [markMayNeedRebuild] + /// then this aborts the rebuild of the associated [HookWidget]. + /// + /// There is no guarantee that this method will be called after [markMayNeedRebuild] + /// was called. + /// Some situations where [shouldRebuild] will not be called: + /// + /// - [setState] was called + /// - a previous hook's [shouldRebuild] returned `true` + /// - the associated [HookWidget] changed. + bool shouldRebuild() => true; + + /// Mark the associated [HookWidget] as **potentially** needing to rebuild. + /// + /// As opposed to [setState], the rebuild is optional and can be cancelled right + /// before [HookWidget.build] is called, by having [shouldRebuild] return false. + void markMayNeedRebuild() { + if (_element._isOptionalRebuild != false) { + _element + .._isOptionalRebuild = true + .._shouldRebuildQueue ??= LinkedList() + .._shouldRebuildQueue.add(_Entry(shouldRebuild)) + ..markNeedsBuild(); + } + } + /// Equivalent of [State.setState] for [HookState] @protected void setState(VoidCallback fn) { - // ignore: invalid_use_of_protected_member - _element.setState(fn); + fn(); + _element + .._isOptionalRebuild = false + ..markNeedsBuild(); } } +class _Entry extends LinkedListEntry<_Entry> { + _Entry(this.value); + final T value; +} + /// An [Element] that uses a [HookWidget] as its configuration. class HookElement extends StatefulElement { - static HookElement _currentContext; - /// Creates an element that uses the given widget as its configuration. HookElement(HookWidget widget) : super(widget); + static HookElement _currentContext; Iterator _currentHook; int _hookIndex; List _hooks; + LinkedList<_Entry> _shouldRebuildQueue; + bool _isOptionalRebuild = false; + Widget _buildCache; bool _didFinishBuildOnce = false; bool _debugDidReassemble; bool _debugShouldDispose; bool _debugIsInitHook; + @override + void update(StatefulWidget newWidget) { + _isOptionalRebuild = false; + super.update(newWidget); + } + + @override + void didChangeDependencies() { + _isOptionalRebuild = false; + super.didChangeDependencies(); + } + @override HookWidget get widget => super.widget as HookWidget; @override Widget build() { + final mustRebuild = _isOptionalRebuild != true || + _shouldRebuildQueue.any((cb) => cb.value()); + + _isOptionalRebuild = null; + _shouldRebuildQueue?.clear(); + + if (!mustRebuild) { + return _buildCache; + } + _currentHook = _hooks?.iterator; // first iterator always has null _currentHook?.moveNext(); @@ -274,21 +336,23 @@ class HookElement extends StatefulElement { _debugIsInitHook = false; _debugDidReassemble ??= false; return true; - }()); + }(), ''); HookElement._currentContext = this; - final result = super.build(); + _buildCache = super.build(); HookElement._currentContext = null; // dispose removed items assert(() { - if (!debugHotReloadHooksEnabled) return true; + if (!debugHotReloadHooksEnabled) { + return true; + } if (_debugDidReassemble && _hooks != null) { - for (var i = _hookIndex; i < _hooks.length;) { - _hooks.removeAt(i).dispose(); + while (_hookIndex < _hooks.length) { + _hooks.removeAt(_hookIndex).dispose(); } } return true; - }()); + }(), ''); assert(_hookIndex == (_hooks?.length ?? 0), ''' Build for $widget finished with less hooks used than a previous build. Used $_hookIndex hooks while a previous build had ${_hooks.length}. @@ -296,12 +360,14 @@ This may happen if the call to `Hook.use` is made under some condition. '''); assert(() { - if (!debugHotReloadHooksEnabled) return true; + if (!debugHotReloadHooksEnabled) { + return true; + } _debugDidReassemble = false; return true; - }()); + }(), ''); _didFinishBuildOnce = true; - return result; + return _buildCache; } /// A read-only list of all hooks available. @@ -311,9 +377,13 @@ This may happen if the call to `Hook.use` is made under some condition. List get debugHooks => List.unmodifiable(_hooks); @override - T dependOnInheritedWidgetOfExactType( - {Object aspect}) { - assert(!_debugIsInitHook); + T dependOnInheritedWidgetOfExactType({ + Object aspect, + }) { + assert( + !_debugIsInitHook, + 'Cannot listen to inherited widgets inside HookState.initState. Use HookState.build instead', + ); return super.dependOnInheritedWidgetOfExactType(aspect: aspect); } @@ -389,21 +459,24 @@ This may happen if the call to `Hook.use` is made under some condition. } } return true; - }()); + }(), ''); } R _use(Hook hook) { HookState> hookState; // first build if (_currentHook == null) { - assert(_debugDidReassemble || !_didFinishBuildOnce); + assert(_debugDidReassemble || !_didFinishBuildOnce, + 'No previous hook found at $_hookIndex, is a hook wrapped in a `if`?'); hookState = _createHookState(hook); _hooks ??= []; _hooks.add(hookState); } else { // recreate states on hot-reload of the order changed assert(() { - if (!debugHotReloadHooksEnabled) return true; + if (!debugHotReloadHooksEnabled) { + return true; + } if (!_debugDidReassemble) { return true; } @@ -423,13 +496,14 @@ This may happen if the call to `Hook.use` is made under some condition. hookState = _pushHook(hook); } return true; - }()); + }(), ''); if (!_didFinishBuildOnce && _currentHook.current == null) { hookState = _pushHook(hook); _currentHook.moveNext(); } else { - assert(_currentHook.current != null); - assert(_debugTypesAreRight(hook)); + assert(_currentHook.current != null, + 'No previous hook found at $_hookIndex, is a hook wrapped in a `if`?'); + assert(_debugTypesAreRight(hook), ''); if (_currentHook.current.hook == hook) { hookState = _currentHook.current as HookState>; @@ -454,27 +528,28 @@ This may happen if the call to `Hook.use` is made under some condition. HookState> _replaceHookAt(int index, Hook hook) { _hooks.removeAt(_hookIndex).dispose(); - var hookState = _createHookState(hook); + final hookState = _createHookState(hook); _hooks.insert(_hookIndex, hookState); return hookState; } HookState> _insertHookAt(int index, Hook hook) { - var hookState = _createHookState(hook); + final hookState = _createHookState(hook); _hooks.insert(index, hookState); _resetsIterator(hookState); return hookState; } HookState> _pushHook(Hook hook) { - var hookState = _createHookState(hook); + final hookState = _createHookState(hook); _hooks.add(hookState); _resetsIterator(hookState); return hookState; } bool _debugTypesAreRight(Hook hook) { - assert(_currentHook.current.hook.runtimeType == hook.runtimeType); + assert(_currentHook.current.hook.runtimeType == hook.runtimeType, + 'The previous and new hooks at index $_hookIndex do not match'); return true; } @@ -490,16 +565,16 @@ This may happen if the call to `Hook.use` is made under some condition. assert(() { _debugIsInitHook = true; return true; - }()); + }(), ''); final state = hook.createState() - .._element = this.state + .._element = this .._hook = hook ..initHook(); assert(() { _debugIsInitHook = false; return true; - }()); + }(), ''); return state; } @@ -548,21 +623,21 @@ BuildContext useContext() { /// A [HookWidget] that defer its [HookWidget.build] to a callback class HookBuilder extends HookWidget { - /// The callback used by [HookBuilder] to create a widget. - /// - /// If a [Hook] asks for a rebuild, [builder] will be called again. - /// [builder] must not return `null`. - final Widget Function(BuildContext context) builder; - /// Creates a widget that delegates its build to a callback. /// /// The [builder] argument must not be null. const HookBuilder({ @required this.builder, Key key, - }) : assert(builder != null), + }) : assert(builder != null, '`builder` cannot be null'), super(key: key); + /// The callback used by [HookBuilder] to create a widget. + /// + /// If a [Hook] asks for a rebuild, [builder] will be called again. + /// [builder] must not return `null`. + final Widget Function(BuildContext context) builder; + @override Widget build(BuildContext context) => builder(context); } diff --git a/lib/src/hooks.dart b/lib/src/hooks.dart index bbec06e9..45cb6731 100644 --- a/lib/src/hooks.dart +++ b/lib/src/hooks.dart @@ -3,7 +3,8 @@ import 'dart:async'; import 'package:flutter/foundation.dart'; import 'package:flutter/scheduler.dart'; import 'package:flutter/widgets.dart'; -import 'package:flutter_hooks/src/framework.dart'; + +import 'framework.dart'; part 'animation.dart'; part 'async.dart'; diff --git a/lib/src/listenable.dart b/lib/src/listenable.dart index b80db47f..f46b419a 100644 --- a/lib/src/listenable.dart +++ b/lib/src/listenable.dart @@ -21,9 +21,10 @@ T useListenable(T listenable) { } class _ListenableHook extends Hook { - final Listenable listenable; + const _ListenableHook(this.listenable) + : assert(listenable != null, 'listenable cannot be null'); - const _ListenableHook(this.listenable) : assert(listenable != null); + final Listenable listenable; @override _ListenableStateHook createState() => _ListenableStateHook(); @@ -74,11 +75,11 @@ ValueNotifier useValueNotifier([T intialData, List keys]) { } class _ValueNotifierHook extends Hook> { - final T initialData; - const _ValueNotifierHook({List keys, this.initialData}) : super(keys: keys); + final T initialData; + @override _UseValueNotiferHookState createState() => _UseValueNotiferHookState(); } diff --git a/lib/src/misc.dart b/lib/src/misc.dart index 5c34a1ae..c7d6d3ac 100644 --- a/lib/src/misc.dart +++ b/lib/src/misc.dart @@ -43,13 +43,13 @@ Store useReducer( } class _ReducerdHook extends Hook> { + const _ReducerdHook(this.reducer, {this.initialState, this.initialAction}) + : assert(reducer != null, 'reducer cannot be null'); + final Reducer reducer; final State initialState; final Action initialAction; - const _ReducerdHook(this.reducer, {this.initialState, this.initialAction}) - : assert(reducer != null); - @override _ReducerdHookState createState() => _ReducerdHookState(); @@ -65,17 +65,16 @@ class _ReducerdHookState void initHook() { super.initHook(); state = hook.reducer(hook.initialState, hook.initialAction); - assert(state != null); + // TODO support null + assert(state != null, 'reducers cannot return null'); } @override void dispatch(Action action) { - final res = hook.reducer(state, action); - assert(res != null); - if (state != res) { - setState(() { - state = res; - }); + final newState = hook.reducer(state, action); + assert(newState != null, 'recuders cannot return null'); + if (state != newState) { + setState(() => state = newState); } } @@ -91,7 +90,7 @@ T usePrevious(T val) { } class _PreviousHook extends Hook { - _PreviousHook(this.value); + const _PreviousHook(this.value); final T value; @@ -121,13 +120,14 @@ void useReassemble(VoidCallback callback) { assert(() { Hook.use(_ReassembleHook(callback)); return true; - }()); + }(), ''); } class _ReassembleHook extends Hook { - final VoidCallback callback; + const _ReassembleHook(this.callback) + : assert(callback != null, 'callback cannot be null'); - _ReassembleHook(this.callback) : assert(callback != null); + final VoidCallback callback; @override _ReassembleHookState createState() => _ReassembleHookState(); diff --git a/lib/src/primitives.dart b/lib/src/primitives.dart index 2b1e4b42..3264f665 100644 --- a/lib/src/primitives.dart +++ b/lib/src/primitives.dart @@ -15,14 +15,15 @@ T useMemoized(T Function() valueBuilder, } class _MemoizedHook extends Hook { - final T Function() valueBuilder; - - const _MemoizedHook(this.valueBuilder, - {List keys = const []}) - : assert(valueBuilder != null), - assert(keys != null), + const _MemoizedHook( + this.valueBuilder, { + List keys = const [], + }) : assert(valueBuilder != null, 'valueBuilder cannot be null'), + assert(keys != null, 'keys cannot be null'), super(keys: keys); + final T Function() valueBuilder; + @override _MemoizedHookState createState() => _MemoizedHookState(); } @@ -61,17 +62,20 @@ class _MemoizedHookState extends HookState> { /// controller.forward(); /// }); /// ``` -R useValueChanged(T value, R valueChange(T oldValue, R oldResult)) { +R useValueChanged( + T value, + R Function(T oldValue, R oldResult) valueChange, +) { return Hook.use(_ValueChangedHook(value, valueChange)); } class _ValueChangedHook extends Hook { + const _ValueChangedHook(this.value, this.valueChanged) + : assert(valueChanged != null, 'valueChanged cannot be null'); + final R Function(T oldValue, R oldResult) valueChanged; final T value; - const _ValueChangedHook(this.value, this.valueChanged) - : assert(valueChanged != null); - @override _ValueChangedHookState createState() => _ValueChangedHookState(); } @@ -128,12 +132,12 @@ void useEffect(Dispose Function() effect, [List keys]) { } class _EffectHook extends Hook { - final Dispose Function() effect; - const _EffectHook(this.effect, [List keys]) - : assert(effect != null), + : assert(effect != null, 'effect cannot be null'), super(keys: keys); + final Dispose Function() effect; + @override _EffectHookState createState() => _EffectHookState(); } @@ -207,10 +211,10 @@ ValueNotifier useState([T initialData]) { } class _StateHook extends Hook> { - final T initialData; - const _StateHook({this.initialData}); + final T initialData; + @override _StateHookState createState() => _StateHookState(); } diff --git a/lib/src/text_controller.dart b/lib/src/text_controller.dart index eb8b33be..3c6828ba 100644 --- a/lib/src/text_controller.dart +++ b/lib/src/text_controller.dart @@ -54,18 +54,20 @@ class _TextEditingControllerHookCreator { const useTextEditingController = _TextEditingControllerHookCreator(); class _TextEditingControllerHook extends Hook { - final String initialText; - final TextEditingValue initialValue; - - _TextEditingControllerHook(this.initialText, this.initialValue, - [List keys]) - : assert( + const _TextEditingControllerHook( + this.initialText, + this.initialValue, [ + List keys, + ]) : assert( initialText == null || initialValue == null, "initialText and intialValue can't both be set on a call to " 'useTextEditingController!', ), super(keys: keys); + final String initialText; + final TextEditingValue initialValue; + @override _TextEditingControllerHookState createState() { return _TextEditingControllerHookState(); diff --git a/pubspec.lock b/pubspec.lock deleted file mode 100644 index c1c5dd3b..00000000 --- a/pubspec.lock +++ /dev/null @@ -1,146 +0,0 @@ -# Generated by pub -# See https://dart.dev/tools/pub/glossary#lockfile -packages: - async: - dependency: transitive - description: - name: async - url: "https://pub.dartlang.org" - source: hosted - version: "2.4.1" - boolean_selector: - dependency: transitive - description: - name: boolean_selector - url: "https://pub.dartlang.org" - source: hosted - version: "2.0.0" - charcode: - dependency: transitive - description: - name: charcode - url: "https://pub.dartlang.org" - source: hosted - version: "1.1.3" - collection: - dependency: transitive - description: - name: collection - url: "https://pub.dartlang.org" - source: hosted - version: "1.14.12" - flutter: - dependency: "direct main" - description: flutter - source: sdk - version: "0.0.0" - flutter_test: - dependency: "direct dev" - description: flutter - source: sdk - version: "0.0.0" - matcher: - dependency: transitive - description: - name: matcher - url: "https://pub.dartlang.org" - source: hosted - version: "0.12.6" - meta: - dependency: transitive - description: - name: meta - url: "https://pub.dartlang.org" - source: hosted - version: "1.1.8" - mockito: - dependency: "direct dev" - description: - name: mockito - url: "https://pub.dartlang.org" - source: hosted - version: "4.1.1" - path: - dependency: transitive - description: - name: path - url: "https://pub.dartlang.org" - source: hosted - version: "1.6.4" - pedantic: - dependency: "direct dev" - description: - name: pedantic - url: "https://pub.dartlang.org" - source: hosted - version: "1.9.0" - quiver: - dependency: transitive - description: - name: quiver - url: "https://pub.dartlang.org" - source: hosted - version: "2.1.3" - sky_engine: - dependency: transitive - description: flutter - source: sdk - version: "0.0.99" - source_span: - dependency: transitive - description: - name: source_span - url: "https://pub.dartlang.org" - source: hosted - version: "1.7.0" - stack_trace: - dependency: transitive - description: - name: stack_trace - url: "https://pub.dartlang.org" - source: hosted - version: "1.9.3" - stream_channel: - dependency: transitive - description: - name: stream_channel - url: "https://pub.dartlang.org" - source: hosted - version: "2.0.0" - string_scanner: - dependency: transitive - description: - name: string_scanner - url: "https://pub.dartlang.org" - source: hosted - version: "1.0.5" - term_glyph: - dependency: transitive - description: - name: term_glyph - url: "https://pub.dartlang.org" - source: hosted - version: "1.1.0" - test_api: - dependency: transitive - description: - name: test_api - url: "https://pub.dartlang.org" - source: hosted - version: "0.2.15" - typed_data: - dependency: transitive - description: - name: typed_data - url: "https://pub.dartlang.org" - source: hosted - version: "1.1.6" - vector_math: - dependency: transitive - description: - name: vector_math - url: "https://pub.dartlang.org" - source: hosted - version: "2.0.8" -sdks: - dart: ">=2.7.0 <3.0.0" diff --git a/pubspec.yaml b/pubspec.yaml index 819fa585..a1e7820f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -3,7 +3,7 @@ description: A flutter implementation of React hooks. It adds a new kind of widg homepage: https://github.com/rrousselGit/flutter_hooks author: Remi Rousselet -version: 0.9.0 +version: 0.10.0-dev environment: sdk: ">=2.7.0 <3.0.0" @@ -15,5 +15,4 @@ dependencies: dev_dependencies: flutter_test: sdk: flutter - pedantic: ^1.4.0 mockito: ">=4.0.0 <5.0.0" diff --git a/test/focus_test.dart b/test/focus_test.dart index 67955652..aec2bf7f 100644 --- a/test/focus_test.dart +++ b/test/focus_test.dart @@ -17,7 +17,7 @@ void main() { // ignore: invalid_use_of_protected_member expect(focusNode.hasListeners, isFalse); - var previous = focusNode; + final previousValue = focusNode; await tester.pumpWidget( HookBuilder(builder: (_) { @@ -26,7 +26,7 @@ void main() { }), ); - expect(previous, focusNode); + expect(previousValue, focusNode); // ignore: invalid_use_of_protected_member expect(focusNode.hasListeners, isFalse); diff --git a/test/hook_builder_test.dart b/test/hook_builder_test.dart index 558f54f7..b722993d 100644 --- a/test/hook_builder_test.dart +++ b/test/hook_builder_test.dart @@ -11,7 +11,8 @@ void main() { return Container(); }); - final createBuilder = () => HookBuilder(builder: fn.call); + Widget createBuilder() => HookBuilder(builder: fn.call); + final _builder = createBuilder(); await tester.pumpWidget(_builder); diff --git a/test/hook_widget_test.dart b/test/hook_widget_test.dart index 47fdc5fd..71cff077 100644 --- a/test/hook_widget_test.dart +++ b/test/hook_widget_test.dart @@ -29,15 +29,17 @@ void main() { final reassemble = Func0(); final builder = Func1(); - final createHook = () => HookTest( - build: build.call, - dispose: dispose.call, - didUpdateHook: didUpdateHook.call, - reassemble: reassemble.call, - initHook: initHook.call, - didBuild: didBuild, - deactivate: deactivate, - ); + HookTest createHook() { + return HookTest( + build: build.call, + dispose: dispose.call, + didUpdateHook: didUpdateHook.call, + reassemble: reassemble.call, + initHook: initHook.call, + didBuild: didBuild, + deactivate: deactivate, + ); + } void verifyNoMoreHookInteration() { verifyNoMoreInteractions(build); @@ -66,98 +68,102 @@ void main() { final state = ValueNotifier(false); final deactivate1 = Func0(); final deactivate2 = Func0(); - await tester.pumpWidget(Directionality( - textDirection: TextDirection.rtl, - child: ValueListenableBuilder( + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.rtl, + child: ValueListenableBuilder( valueListenable: state, - builder: (context, bool value, _) => Stack(children: [ - Container( - key: const Key('1'), - child: HookBuilder( - key: value ? _key2 : _key1, - builder: (context) { - Hook.use(HookTest(deactivate: deactivate1)); - return Container(); - }, - ), - ), - HookBuilder( - key: !value ? _key2 : _key1, + builder: (context, value, _) { + return Stack(children: [ + Container( + key: const Key('1'), + child: HookBuilder( + key: value ? _key2 : _key1, builder: (context) { - Hook.use(HookTest(deactivate: deactivate2)); + Hook.use(HookTest(deactivate: deactivate1)); return Container(); }, ), - ])), - )); + ), + HookBuilder( + key: !value ? _key2 : _key1, + builder: (context) { + Hook.use(HookTest(deactivate: deactivate2)); + return Container(); + }, + ), + ]); + }, + ), + ), + ); + await tester.pump(); + verifyNever(deactivate1()); verifyNever(deactivate2()); state.value = true; + await tester.pump(); + verifyInOrder([ deactivate1.call(), deactivate2.call(), ]); + await tester.pump(); + verifyNoMoreInteractions(deactivate1); verifyNoMoreInteractions(deactivate2); }); testWidgets('should call other deactivates even if one fails', (tester) async { - final deactivate2 = Func0(); - final _key = GlobalKey(); final onError = Func1(); final oldOnError = FlutterError.onError; FlutterError.onError = onError; + final errorBuilder = ErrorWidget.builder; ErrorWidget.builder = Func1(); when(ErrorWidget.builder(any)).thenReturn(Container()); - try { - when(deactivate2.call()).thenThrow(42); - when(builder.call(any)).thenAnswer((invocation) { - Hook.use(createHook()); + + final deactivate = Func0(); + when(deactivate.call()).thenThrow(42); + final deactivate2 = Func0(); + + final _key = GlobalKey(); + + final widget = HookBuilder( + key: _key, + builder: (context) { + Hook.use(HookTest(deactivate: deactivate)); Hook.use(HookTest(deactivate: deactivate2)); return Container(); - }); - await tester.pumpWidget(Column( - children: [ - Row( - children: [ - HookBuilder( - key: _key, - builder: builder.call, - ) - ], - ), - Container( - child: const SizedBox(), - ), - ], - )); + }, + ); - await tester.pumpWidget(Column( - children: [ - Row( - children: [], - ), - Container( - child: HookBuilder( - key: _key, - builder: builder.call, - ), - ), - ], - )); + try { + await tester.pumpWidget(SizedBox(child: widget)); + + verifyNoMoreInteractions(deactivate); + verifyNoMoreInteractions(deactivate2); + + await tester.pumpWidget(widget); + verifyInOrder([ + deactivate(), + deactivate2(), + ]); + + verify(onError.call(any)).called(1); + verifyNoMoreInteractions(deactivate); + verifyNoMoreInteractions(deactivate2); + } finally { // reset the exception because after the test // flutter tries to deactivate the widget and it causes // and exception - when(deactivate2.call()).thenAnswer((_) {}); - verify(onError.call(any)).called((int x) => x > 1); - verify(deactivate.call()).called(1); - } finally { + when(deactivate.call()).thenAnswer((_) {}); FlutterError.onError = oldOnError; ErrorWidget.builder = errorBuilder; } @@ -176,8 +182,8 @@ void main() { testWidgets('allows using inherited widgets outside of initHook', (tester) async { when(build(any)).thenAnswer((invocation) { - invocation.positionalArguments.first as BuildContext - ..dependOnInheritedWidgetOfExactType(); + final context = invocation.positionalArguments.first as BuildContext; + context.dependOnInheritedWidgetOfExactType(); return null; }); diff --git a/test/memoized_test.dart b/test/memoized_test.dart index e49d1ce7..e2dc0281 100644 --- a/test/memoized_test.dart +++ b/test/memoized_test.dart @@ -16,7 +16,7 @@ void main() { testWidgets('invalid parameters', (tester) async { await tester.pumpWidget(HookBuilder(builder: (context) { - useMemoized(null); + useMemoized(null); return Container(); })); expect(tester.takeException(), isAssertionError); @@ -198,7 +198,7 @@ void main() { }); testWidgets( - 'memoized parameter reference do not change don\'t call valueBuilder', + "memoized parameter reference do not change don't call valueBuilder", (tester) async { int result; final parameters = []; diff --git a/test/mock.dart b/test/mock.dart index 135bf933..a2e449cd 100644 --- a/test/mock.dart +++ b/test/mock.dart @@ -1,3 +1,5 @@ +// ignore_for_file: one_member_abstracts + import 'package:flutter/widgets.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -26,15 +28,7 @@ abstract class _Func2 { class Func2 extends Mock implements _Func2 {} class HookTest extends Hook { - final R Function(BuildContext context) build; - final void Function() dispose; - final void Function() didBuild; - final void Function() initHook; - final void Function() deactivate; - final void Function(HookTest previousHook) didUpdateHook; - final void Function() reassemble; - final HookStateTest Function() createStateFn; - + // ignore: prefer_const_constructors_in_immutables HookTest({ this.build, this.dispose, @@ -47,6 +41,15 @@ class HookTest extends Hook { List keys, }) : super(keys: keys); + final R Function(BuildContext context) build; + final void Function() dispose; + final void Function() didBuild; + final void Function() initHook; + final void Function() deactivate; + final void Function(HookTest previousHook) didUpdateHook; + final void Function() reassemble; + final HookStateTest Function() createStateFn; + @override HookStateTest createState() => createStateFn != null ? createStateFn() : HookStateTest(); @@ -123,11 +126,11 @@ Element _rootOf(Element element) { void hotReload(WidgetTester tester) { final root = _rootOf(tester.allElements.first); - TestWidgetsFlutterBinding.ensureInitialized().buildOwner..reassemble(root); + TestWidgetsFlutterBinding.ensureInitialized().buildOwner.reassemble(root); } Future expectPump( - Future pump(), + Future Function() pump, dynamic matcher, { String reason, dynamic skip, diff --git a/test/pre_build_abort_test.dart b/test/pre_build_abort_test.dart new file mode 100644 index 00000000..c076e0dd --- /dev/null +++ b/test/pre_build_abort_test.dart @@ -0,0 +1,291 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/src/widgets/framework.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'mock.dart'; + +void main() { + testWidgets('shouldRebuild defaults to true', (tester) async { + MayRebuildState first; + var buildCount = 0; + + await tester.pumpWidget( + HookBuilder(builder: (c) { + buildCount++; + first = Hook.use(const MayRebuild()); + + return Container(); + }), + ); + + expect(buildCount, 1); + + first.markMayNeedRebuild(); + await tester.pump(); + + expect(buildCount, 2); + }); + testWidgets('can queue multiple mayRebuilds at once', (tester) async { + final firstSpy = ShouldRebuildMock(); + final secondSpy = ShouldRebuildMock(); + MayRebuildState first; + MayRebuildState second; + var buildCount = 0; + + await tester.pumpWidget( + HookBuilder(builder: (c) { + buildCount++; + first = Hook.use(MayRebuild(firstSpy)); + second = Hook.use(MayRebuild(secondSpy)); + + return Container(); + }), + ); + + verifyNoMoreInteractions(firstSpy); + verifyNoMoreInteractions(secondSpy); + expect(buildCount, 1); + + first.markMayNeedRebuild(); + when(firstSpy()).thenReturn(false); + second.markMayNeedRebuild(); + when(secondSpy()).thenReturn(false); + + await tester.pump(); + + expect(buildCount, 1); + verifyInOrder([ + firstSpy(), + secondSpy(), + ]); + verifyNoMoreInteractions(firstSpy); + verifyNoMoreInteractions(secondSpy); + + first.markMayNeedRebuild(); + when(firstSpy()).thenReturn(true); + second.markMayNeedRebuild(); + when(secondSpy()).thenReturn(false); + + await tester.pump(); + + expect(buildCount, 2); + verify(firstSpy()).called(1); + verifyNoMoreInteractions(firstSpy); + verifyNoMoreInteractions(secondSpy); + }); + testWidgets('pre-build-abort', (tester) async { + var buildCount = 0; + final notifier = ValueNotifier(0); + + await tester.pumpWidget( + HookBuilder(builder: (c) { + buildCount++; + final value = Hook.use(IsPositiveHook(notifier)); + + return Text('$value', textDirection: TextDirection.ltr); + }), + ); + + expect(buildCount, 1); + expect(find.text('true'), findsOneWidget); + expect(find.text('false'), findsNothing); + + notifier.value = -10; + + await tester.pump(); + + expect(buildCount, 2); + expect(find.text('false'), findsOneWidget); + expect(find.text('true'), findsNothing); + + notifier.value = -20; + + await tester.pump(); + + expect(buildCount, 2); + expect(find.text('false'), findsOneWidget); + expect(find.text('true'), findsNothing); + + notifier.value = 20; + + await tester.pump(); + + expect(buildCount, 3); + expect(find.text('true'), findsOneWidget); + expect(find.text('false'), findsNothing); + }); + testWidgets('setState then markMayNeedBuild still force build', + (tester) async { + var buildCount = 0; + final notifier = ValueNotifier(0); + + await tester.pumpWidget( + HookBuilder(builder: (c) { + buildCount++; + useListenable(notifier); + final value = Hook.use(IsPositiveHook(notifier)); + + return Text('$value', textDirection: TextDirection.ltr); + }), + ); + + expect(buildCount, 1); + expect(find.text('true'), findsOneWidget); + expect(find.text('false'), findsNothing); + + notifier.value++; + await tester.pump(); + + expect(buildCount, 2); + expect(find.text('true'), findsOneWidget); + expect(find.text('false'), findsNothing); + }); + testWidgets('markMayNeedBuild then widget rebuild forces build', + (tester) async { + var buildCount = 0; + final notifier = ValueNotifier(0); + + Widget build() { + return HookBuilder(builder: (c) { + buildCount++; + final value = Hook.use(IsPositiveHook(notifier)); + + return Text('$value', textDirection: TextDirection.ltr); + }); + } + + await tester.pumpWidget(build()); + + expect(buildCount, 1); + expect(find.text('true'), findsOneWidget); + expect(find.text('false'), findsNothing); + + notifier.value++; + await tester.pumpWidget(build()); + + expect(buildCount, 2); + expect(find.text('true'), findsOneWidget); + expect(find.text('false'), findsNothing); + }); + testWidgets('markMayNeedBuild then didChangeDepencies forces build', + (tester) async { + var buildCount = 0; + final notifier = ValueNotifier(0); + + final child = HookBuilder(builder: (c) { + buildCount++; + Directionality.of(c); + final value = Hook.use(IsPositiveHook(notifier)); + + return Text('$value', textDirection: TextDirection.ltr); + }); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: child, + ), + ); + + expect(buildCount, 1); + expect(find.text('true'), findsOneWidget); + expect(find.text('false'), findsNothing); + + notifier.value++; + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.rtl, + child: child, + ), + ); + + expect(buildCount, 2); + expect(find.text('true'), findsOneWidget); + expect(find.text('false'), findsNothing); + }); +} + +class IsPositiveHook extends Hook { + const IsPositiveHook(this.notifier); + + final ValueNotifier notifier; + + @override + IsPositiveHookState createState() { + return IsPositiveHookState(); + } +} + +class IsPositiveHookState extends HookState { + bool dirty = true; + bool value; + + @override + void initHook() { + super.initHook(); + value = hook.notifier.value >= 0; + hook.notifier.addListener(listener); + } + + void listener() { + dirty = true; + markMayNeedRebuild(); + } + + @override + bool shouldRebuild() { + if (dirty) { + dirty = false; + final newValue = hook.notifier.value >= 0; + if (newValue != value) { + value = newValue; + return true; + } + } + return false; + } + + @override + bool build(BuildContext context) { + if (dirty) { + dirty = false; + value = hook.notifier.value >= 0; + } + return value; + } + + @override + void dispose() { + hook.notifier.removeListener(listener); + super.dispose(); + } +} + +class MayRebuild extends Hook { + const MayRebuild([this.shouldRebuild]); + + final ShouldRebuildMock shouldRebuild; + + @override + MayRebuildState createState() { + return MayRebuildState(); + } +} + +class MayRebuildState extends HookState { + @override + bool shouldRebuild() { + if (hook.shouldRebuild == null) { + return super.shouldRebuild(); + } + return hook.shouldRebuild(); + } + + @override + MayRebuildState build(BuildContext context) => this; +} + +class ShouldRebuildMock extends Mock { + bool call(); +} diff --git a/test/use_animation_controller_test.dart b/test/use_animation_controller_test.dart index fd078c7c..0f3ddd39 100644 --- a/test/use_animation_controller_test.dart +++ b/test/use_animation_controller_test.dart @@ -68,7 +68,7 @@ void main() { expect(controller.animationBehavior, AnimationBehavior.preserve); expect(controller.debugLabel, 'Foo'); - var previousController = controller; + final previousController = controller; provider = _TickerProvider(); when(provider.createTicker(any)).thenAnswer((_) { return tester @@ -79,11 +79,7 @@ void main() { HookBuilder(builder: (context) { controller = useAnimationController( vsync: provider, - animationBehavior: AnimationBehavior.normal, duration: const Duration(seconds: 2), - initialValue: 0, - lowerBound: 0, - upperBound: 0, debugLabel: 'Bar', ); return Container(); diff --git a/test/use_animation_test.dart b/test/use_animation_test.dart index c80a0214..8b037b30 100644 --- a/test/use_animation_test.dart +++ b/test/use_animation_test.dart @@ -19,12 +19,14 @@ void main() { var listenable = AnimationController(vsync: tester); double result; - pump() => tester.pumpWidget(HookBuilder( - builder: (context) { - result = useAnimation(listenable); - return Container(); - }, - )); + Future pump() { + return tester.pumpWidget(HookBuilder( + builder: (context) { + result = useAnimation(listenable); + return Container(); + }, + )); + } await pump(); diff --git a/test/use_effect_test.dart b/test/use_effect_test.dart index 49d9ef1f..d9578601 100644 --- a/test/use_effect_test.dart +++ b/test/use_effect_test.dart @@ -3,17 +3,17 @@ import 'package:flutter_hooks/flutter_hooks.dart'; import 'mock.dart'; -final effect = Func0(); -final unrelated = Func0(); -List parameters; +void main() { + final effect = Func0(); + final unrelated = Func0(); + List parameters; -Widget builder() => HookBuilder(builder: (context) { - useEffect(effect.call, parameters); - unrelated.call(); - return Container(); - }); + Widget builder() => HookBuilder(builder: (context) { + useEffect(effect.call, parameters); + unrelated.call(); + return Container(); + }); -void main() { tearDown(() { parameters = null; reset(unrelated); @@ -35,11 +35,13 @@ void main() { final dispose = Func0(); when(effect.call()).thenReturn(dispose.call); - builder() => HookBuilder(builder: (context) { - useEffect(effect.call); - unrelated.call(); - return Container(); - }); + Widget builder() { + return HookBuilder(builder: (context) { + useEffect(effect.call); + unrelated.call(); + return Container(); + }); + } await tester.pumpWidget(builder()); @@ -180,10 +182,12 @@ void main() { final effect = Func0(); List parameters; - builder() => HookBuilder(builder: (context) { - useEffect(effect.call, parameters); - return Container(); - }); + Widget builder() { + return HookBuilder(builder: (context) { + useEffect(effect.call, parameters); + return Container(); + }); + } parameters = ['foo']; final disposerA = Func0(); diff --git a/test/use_future_test.dart b/test/use_future_test.dart index 89389e4a..e82f3377 100644 --- a/test/use_future_test.dart +++ b/test/use_future_test.dart @@ -59,8 +59,7 @@ void main() { }; } - testWidgets('gracefully handles transition from null future', - (WidgetTester tester) async { + testWidgets('gracefully handles transition from null future', (tester) async { await tester.pumpWidget(HookBuilder(builder: snapshotText(null))); expect(find.text('AsyncSnapshot(ConnectionState.none, null, null)'), findsOneWidget); @@ -71,8 +70,7 @@ void main() { find.text('AsyncSnapshot(ConnectionState.waiting, null, null)'), findsOneWidget); }); - testWidgets('gracefully handles transition to null future', - (WidgetTester tester) async { + testWidgets('gracefully handles transition to null future', (tester) async { final completer = Completer(); await tester .pumpWidget(HookBuilder(builder: snapshotText(completer.future))); @@ -87,8 +85,7 @@ void main() { expect(find.text('AsyncSnapshot(ConnectionState.none, null, null)'), findsOneWidget); }); - testWidgets('gracefully handles transition to other future', - (WidgetTester tester) async { + testWidgets('gracefully handles transition to other future', (tester) async { final completerA = Completer(); final completerB = Completer(); await tester @@ -107,8 +104,7 @@ void main() { expect(find.text('AsyncSnapshot(ConnectionState.done, B, null)'), findsOneWidget); }); - testWidgets('tracks life-cycle of Future to success', - (WidgetTester tester) async { + testWidgets('tracks life-cycle of Future to success', (tester) async { final completer = Completer(); await tester .pumpWidget(HookBuilder(builder: snapshotText(completer.future))); @@ -121,8 +117,7 @@ void main() { find.text('AsyncSnapshot(ConnectionState.done, hello, null)'), findsOneWidget); }); - testWidgets('tracks life-cycle of Future to error', - (WidgetTester tester) async { + testWidgets('tracks life-cycle of Future to error', (tester) async { final completer = Completer(); await tester .pumpWidget(HookBuilder(builder: snapshotText(completer.future))); @@ -134,8 +129,7 @@ void main() { expect(find.text('AsyncSnapshot(ConnectionState.done, null, bad)'), findsOneWidget); }); - testWidgets('runs the builder using given initial data', - (WidgetTester tester) async { + testWidgets('runs the builder using given initial data', (tester) async { await tester.pumpWidget(HookBuilder( builder: snapshotText( null, @@ -145,8 +139,7 @@ void main() { expect(find.text('AsyncSnapshot(ConnectionState.none, I, null)'), findsOneWidget); }); - testWidgets('ignores initialData when reconfiguring', - (WidgetTester tester) async { + testWidgets('ignores initialData when reconfiguring', (tester) async { await tester.pumpWidget(HookBuilder( builder: snapshotText( null, diff --git a/test/use_listenable_test.dart b/test/use_listenable_test.dart index 6ce5caef..928cae1d 100644 --- a/test/use_listenable_test.dart +++ b/test/use_listenable_test.dart @@ -17,12 +17,14 @@ void main() { testWidgets('useListenable', (tester) async { var listenable = ValueNotifier(0); - pump() => tester.pumpWidget(HookBuilder( - builder: (context) { - useListenable(listenable); - return Container(); - }, - )); + Future pump() { + return tester.pumpWidget(HookBuilder( + builder: (context) { + useListenable(listenable); + return Container(); + }, + )); + } await pump(); diff --git a/test/use_reassemble_test.dart b/test/use_reassemble_test.dart index 2bdd84d4..ab79c91b 100644 --- a/test/use_reassemble_test.dart +++ b/test/use_reassemble_test.dart @@ -14,7 +14,7 @@ void main() { ); }); - testWidgets('hot-reload calls useReassemble\'s callback', (tester) async { + testWidgets("hot-reload calls useReassemble's callback", (tester) async { final reassemble = Func0(); await tester.pumpWidget(HookBuilder(builder: (context) { useReassemble(reassemble); diff --git a/test/use_reducer_test.dart b/test/use_reducer_test.dart index 78f3b81f..a54279ab 100644 --- a/test/use_reducer_test.dart +++ b/test/use_reducer_test.dart @@ -9,12 +9,14 @@ void main() { final reducer = Func2(); Store store; - pump() => tester.pumpWidget(HookBuilder( - builder: (context) { - store = useReducer(reducer.call); - return Container(); - }, - )); + Future pump() { + return tester.pumpWidget(HookBuilder( + builder: (context) { + store = useReducer(reducer.call); + return Container(); + }, + )); + } when(reducer.call(null, null)).thenReturn(0); await pump(); @@ -96,12 +98,14 @@ void main() { final reducer = Func2(); Store store; - pump() => tester.pumpWidget(HookBuilder( - builder: (context) { - store = useReducer(reducer.call); - return Container(); - }, - )); + Future pump() { + return tester.pumpWidget(HookBuilder( + builder: (context) { + store = useReducer(reducer.call); + return Container(); + }, + )); + } when(reducer.call(null, null)).thenReturn(42); diff --git a/test/use_stream_controller_test.dart b/test/use_stream_controller_test.dart index 943d65ca..782af5c7 100644 --- a/test/use_stream_controller_test.dart +++ b/test/use_stream_controller_test.dart @@ -38,8 +38,8 @@ void main() { expect(() => controller.onResume, throwsUnsupportedError); final previousController = controller; - final onListen = () {}; - final onCancel = () {}; + void onListen() {} + void onCancel() {} await tester.pumpWidget(HookBuilder(builder: (context) { controller = useStreamController( sync: true, @@ -75,8 +75,8 @@ void main() { expect(() => controller.onResume, throwsUnsupportedError); final previousController = controller; - final onListen = () {}; - final onCancel = () {}; + void onListen() {} + void onCancel() {} await tester.pumpWidget(HookBuilder(builder: (context) { controller = useStreamController( onCancel: onCancel, diff --git a/test/use_stream_test.dart b/test/use_stream_test.dart index e48cc827..2249592c 100644 --- a/test/use_stream_test.dart +++ b/test/use_stream_test.dart @@ -1,3 +1,5 @@ +// ignore_for_file: close_sinks + import 'dart:async'; import 'package:flutter/widgets.dart'; @@ -61,8 +63,7 @@ void main() { }; } - testWidgets('gracefully handles transition from null stream', - (WidgetTester tester) async { + testWidgets('gracefully handles transition from null stream', (tester) async { await tester.pumpWidget(HookBuilder(builder: snapshotText(null))); expect(find.text('AsyncSnapshot(ConnectionState.none, null, null)'), findsOneWidget); @@ -73,8 +74,7 @@ void main() { find.text('AsyncSnapshot(ConnectionState.waiting, null, null)'), findsOneWidget); }); - testWidgets('gracefully handles transition to null stream', - (WidgetTester tester) async { + testWidgets('gracefully handles transition to null stream', (tester) async { final controller = StreamController(); await tester .pumpWidget(HookBuilder(builder: snapshotText(controller.stream))); @@ -85,8 +85,7 @@ void main() { expect(find.text('AsyncSnapshot(ConnectionState.none, null, null)'), findsOneWidget); }); - testWidgets('gracefully handles transition to other stream', - (WidgetTester tester) async { + testWidgets('gracefully handles transition to other stream', (tester) async { final controllerA = StreamController(); final controllerB = StreamController(); await tester @@ -103,7 +102,7 @@ void main() { findsOneWidget); }); testWidgets('tracks events and errors of stream until completion', - (WidgetTester tester) async { + (tester) async { final controller = StreamController(); await tester .pumpWidget(HookBuilder(builder: snapshotText(controller.stream))); @@ -127,8 +126,7 @@ void main() { expect(find.text('AsyncSnapshot(ConnectionState.done, 4, null)'), findsOneWidget); }); - testWidgets('runs the builder using given initial data', - (WidgetTester tester) async { + testWidgets('runs the builder using given initial data', (tester) async { final controller = StreamController(); await tester.pumpWidget(HookBuilder( builder: snapshotText(controller.stream, initialData: 'I'), @@ -136,8 +134,7 @@ void main() { expect(find.text('AsyncSnapshot(ConnectionState.waiting, I, null)'), findsOneWidget); }); - testWidgets('ignores initialData when reconfiguring', - (WidgetTester tester) async { + testWidgets('ignores initialData when reconfiguring', (tester) async { await tester.pumpWidget(HookBuilder( builder: snapshotText(null, initialData: 'I'), )); diff --git a/test/use_text_editing_controller_test.dart b/test/use_text_editing_controller_test.dart index 24a484b9..32833fa8 100644 --- a/test/use_text_editing_controller_test.dart +++ b/test/use_text_editing_controller_test.dart @@ -32,9 +32,11 @@ void main() { // pump another widget so that the old one gets disposed await tester.pumpWidget(Container()); - expect(() => controller.addListener(null), throwsA((FlutterError error) { - return error.message.contains('disposed'); - })); + expect( + () => controller.addListener(null), + throwsA(isFlutterError.having( + (e) => e.message, 'message', contains('disposed'))), + ); }); testWidgets('respects initial text property', (tester) async { diff --git a/test/use_value_changed_test.dart b/test/use_value_changed_test.dart index 5bfd4a11..f99b3872 100644 --- a/test/use_value_changed_test.dart +++ b/test/use_value_changed_test.dart @@ -9,7 +9,7 @@ void main() { final _useValueChanged = Func2(); String result; - pump() => tester.pumpWidget(HookBuilder( + Future pump() => tester.pumpWidget(HookBuilder( builder: (context) { result = useValueChanged(value, _useValueChanged.call); return Container(); diff --git a/test/use_value_listenable_test.dart b/test/use_value_listenable_test.dart index d15011d2..e2e3308c 100644 --- a/test/use_value_listenable_test.dart +++ b/test/use_value_listenable_test.dart @@ -18,12 +18,14 @@ void main() { var listenable = ValueNotifier(0); int result; - pump() => tester.pumpWidget(HookBuilder( - builder: (context) { - result = useValueListenable(listenable); - return Container(); - }, - )); + Future pump() { + return tester.pumpWidget(HookBuilder( + builder: (context) { + result = useValueListenable(listenable); + return Container(); + }, + )); + } await pump(); diff --git a/test/use_value_notifier_test.dart b/test/use_value_notifier_test.dart index 29bba791..4bc660e1 100644 --- a/test/use_value_notifier_test.dart +++ b/test/use_value_notifier_test.dart @@ -110,8 +110,7 @@ void main() { expect(state, isNot(previous)); }); - testWidgets('instance stays the same when key don\' change', - (tester) async { + testWidgets("instance stays the same when key don' change", (tester) async { ValueNotifier state; ValueNotifier previous; From bdb8024ac4115cdcdcbd5d80cf0a8959b12fd47b Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Fri, 12 Jun 2020 16:55:23 +0100 Subject: [PATCH 125/384] v0.10.0-dev+1 --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index a1e7820f..6ab77824 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -3,7 +3,7 @@ description: A flutter implementation of React hooks. It adds a new kind of widg homepage: https://github.com/rrousselGit/flutter_hooks author: Remi Rousselet -version: 0.10.0-dev +version: 0.10.0-dev+1 environment: sdk: ">=2.7.0 <3.0.0" From 84361cdac9c4db8e4e9babdb17d675d85c6c167d Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Wed, 17 Jun 2020 09:54:09 +0100 Subject: [PATCH 126/384] gitignore --- .gitignore | 2 +- example/.flutter-plugins-dependencies | 2 +- example/pubspec.lock | 504 -------------------------- 3 files changed, 2 insertions(+), 506 deletions(-) delete mode 100644 example/pubspec.lock diff --git a/.gitignore b/.gitignore index 884c2afd..70606c31 100644 --- a/.gitignore +++ b/.gitignore @@ -9,4 +9,4 @@ build/ android/ ios/ /coverage -.pubspec.lock \ No newline at end of file +pubspec.lock \ No newline at end of file diff --git a/example/.flutter-plugins-dependencies b/example/.flutter-plugins-dependencies index 1cdfcc0d..1296f2cc 100644 --- a/example/.flutter-plugins-dependencies +++ b/example/.flutter-plugins-dependencies @@ -1 +1 @@ -{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"shared_preferences","path":"/Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/shared_preferences-0.4.3/","dependencies":[]}],"android":[{"name":"shared_preferences","path":"/Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/shared_preferences-0.4.3/","dependencies":[]}],"macos":[],"linux":[],"windows":[],"web":[]},"dependencyGraph":[{"name":"shared_preferences","dependencies":[]}],"date_created":"2020-06-12 09:30:06.628061","version":"1.19.0-4.0.pre.128"} \ No newline at end of file +{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"shared_preferences","path":"/Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/shared_preferences-0.4.3/","dependencies":[]}],"android":[{"name":"shared_preferences","path":"/Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/shared_preferences-0.4.3/","dependencies":[]}],"macos":[],"linux":[],"windows":[],"web":[]},"dependencyGraph":[{"name":"shared_preferences","dependencies":[]}],"date_created":"2020-06-17 09:08:41.153385","version":"1.20.0-0.0.pre"} \ No newline at end of file diff --git a/example/pubspec.lock b/example/pubspec.lock deleted file mode 100644 index b7f532ec..00000000 --- a/example/pubspec.lock +++ /dev/null @@ -1,504 +0,0 @@ -# Generated by pub -# See https://dart.dev/tools/pub/glossary#lockfile -packages: - analyzer: - dependency: transitive - description: - name: analyzer - url: "https://pub.dartlang.org" - source: hosted - version: "0.38.5" - analyzer_plugin: - dependency: transitive - description: - name: analyzer_plugin - url: "https://pub.dartlang.org" - source: hosted - version: "0.2.1" - args: - dependency: transitive - description: - name: args - url: "https://pub.dartlang.org" - source: hosted - version: "1.5.2" - async: - dependency: transitive - description: - name: async - url: "https://pub.dartlang.org" - source: hosted - version: "2.4.1" - boolean_selector: - dependency: transitive - description: - name: boolean_selector - url: "https://pub.dartlang.org" - source: hosted - version: "2.0.0" - build: - dependency: transitive - description: - name: build - url: "https://pub.dartlang.org" - source: hosted - version: "1.2.1" - build_config: - dependency: transitive - description: - name: build_config - url: "https://pub.dartlang.org" - source: hosted - version: "0.4.1+1" - build_daemon: - dependency: transitive - description: - name: build_daemon - url: "https://pub.dartlang.org" - source: hosted - version: "2.1.0" - build_resolvers: - dependency: transitive - description: - name: build_resolvers - url: "https://pub.dartlang.org" - source: hosted - version: "1.2.1" - build_runner: - dependency: "direct dev" - description: - name: build_runner - url: "https://pub.dartlang.org" - source: hosted - version: "1.7.1" - build_runner_core: - dependency: transitive - description: - name: build_runner_core - url: "https://pub.dartlang.org" - source: hosted - version: "4.1.0" - built_collection: - dependency: "direct main" - description: - name: built_collection - url: "https://pub.dartlang.org" - source: hosted - version: "4.2.2" - built_value: - dependency: "direct main" - description: - name: built_value - url: "https://pub.dartlang.org" - source: hosted - version: "6.8.2" - built_value_generator: - dependency: "direct dev" - description: - name: built_value_generator - url: "https://pub.dartlang.org" - source: hosted - version: "6.8.2" - charcode: - dependency: transitive - description: - name: charcode - url: "https://pub.dartlang.org" - source: hosted - version: "1.1.3" - checked_yaml: - dependency: transitive - description: - name: checked_yaml - url: "https://pub.dartlang.org" - source: hosted - version: "1.0.2" - clock: - dependency: transitive - description: - name: clock - url: "https://pub.dartlang.org" - source: hosted - version: "1.0.1" - code_builder: - dependency: transitive - description: - name: code_builder - url: "https://pub.dartlang.org" - source: hosted - version: "3.2.0" - collection: - dependency: transitive - description: - name: collection - url: "https://pub.dartlang.org" - source: hosted - version: "1.14.12" - convert: - dependency: transitive - description: - name: convert - url: "https://pub.dartlang.org" - source: hosted - version: "2.1.1" - crypto: - dependency: transitive - description: - name: crypto - url: "https://pub.dartlang.org" - source: hosted - version: "2.1.3" - csslib: - dependency: transitive - description: - name: csslib - url: "https://pub.dartlang.org" - source: hosted - version: "0.16.1" - dart_style: - dependency: transitive - description: - name: dart_style - url: "https://pub.dartlang.org" - source: hosted - version: "1.3.3" - fake_async: - dependency: transitive - description: - name: fake_async - url: "https://pub.dartlang.org" - source: hosted - version: "1.1.0" - fixnum: - dependency: transitive - description: - name: fixnum - url: "https://pub.dartlang.org" - source: hosted - version: "0.10.11" - flutter: - dependency: "direct main" - description: flutter - source: sdk - version: "0.0.0" - flutter_hooks: - dependency: "direct main" - description: - path: ".." - relative: true - source: path - version: "0.10.0-dev" - flutter_test: - dependency: "direct dev" - description: flutter - source: sdk - version: "0.0.0" - front_end: - dependency: transitive - description: - name: front_end - url: "https://pub.dartlang.org" - source: hosted - version: "0.1.27" - glob: - dependency: transitive - description: - name: glob - url: "https://pub.dartlang.org" - source: hosted - version: "1.2.0" - graphs: - dependency: transitive - description: - name: graphs - url: "https://pub.dartlang.org" - source: hosted - version: "0.2.0" - html: - dependency: transitive - description: - name: html - url: "https://pub.dartlang.org" - source: hosted - version: "0.14.0+3" - http: - dependency: transitive - description: - name: http - url: "https://pub.dartlang.org" - source: hosted - version: "0.12.0+2" - http_multi_server: - dependency: transitive - description: - name: http_multi_server - url: "https://pub.dartlang.org" - source: hosted - version: "2.1.0" - http_parser: - dependency: transitive - description: - name: http_parser - url: "https://pub.dartlang.org" - source: hosted - version: "3.1.3" - io: - dependency: transitive - description: - name: io - url: "https://pub.dartlang.org" - source: hosted - version: "0.3.3" - js: - dependency: transitive - description: - name: js - url: "https://pub.dartlang.org" - source: hosted - version: "0.6.1+1" - json_annotation: - dependency: transitive - description: - name: json_annotation - url: "https://pub.dartlang.org" - source: hosted - version: "3.0.0" - kernel: - dependency: transitive - description: - name: kernel - url: "https://pub.dartlang.org" - source: hosted - version: "0.3.27" - logging: - dependency: transitive - description: - name: logging - url: "https://pub.dartlang.org" - source: hosted - version: "0.11.3+2" - matcher: - dependency: transitive - description: - name: matcher - url: "https://pub.dartlang.org" - source: hosted - version: "0.12.6" - meta: - dependency: transitive - description: - name: meta - url: "https://pub.dartlang.org" - source: hosted - version: "1.1.8" - mime: - dependency: transitive - description: - name: mime - url: "https://pub.dartlang.org" - source: hosted - version: "0.9.6+3" - node_interop: - dependency: transitive - description: - name: node_interop - url: "https://pub.dartlang.org" - source: hosted - version: "1.0.3" - node_io: - dependency: transitive - description: - name: node_io - url: "https://pub.dartlang.org" - source: hosted - version: "1.0.1+2" - package_config: - dependency: transitive - description: - name: package_config - url: "https://pub.dartlang.org" - source: hosted - version: "1.1.0" - package_resolver: - dependency: transitive - description: - name: package_resolver - url: "https://pub.dartlang.org" - source: hosted - version: "1.0.10" - path: - dependency: transitive - description: - name: path - url: "https://pub.dartlang.org" - source: hosted - version: "1.7.0" - pedantic: - dependency: transitive - description: - name: pedantic - url: "https://pub.dartlang.org" - source: hosted - version: "1.8.0+1" - pool: - dependency: transitive - description: - name: pool - url: "https://pub.dartlang.org" - source: hosted - version: "1.4.0" - provider: - dependency: "direct main" - description: - name: provider - url: "https://pub.dartlang.org" - source: hosted - version: "3.1.0+1" - pub_semver: - dependency: transitive - description: - name: pub_semver - url: "https://pub.dartlang.org" - source: hosted - version: "1.4.2" - pubspec_parse: - dependency: transitive - description: - name: pubspec_parse - url: "https://pub.dartlang.org" - source: hosted - version: "0.1.5" - quiver: - dependency: transitive - description: - name: quiver - url: "https://pub.dartlang.org" - source: hosted - version: "2.1.3" - shared_preferences: - dependency: "direct main" - description: - name: shared_preferences - url: "https://pub.dartlang.org" - source: hosted - version: "0.4.3" - shelf: - dependency: transitive - description: - name: shelf - url: "https://pub.dartlang.org" - source: hosted - version: "0.7.5" - shelf_web_socket: - dependency: transitive - description: - name: shelf_web_socket - url: "https://pub.dartlang.org" - source: hosted - version: "0.2.3" - sky_engine: - dependency: transitive - description: flutter - source: sdk - version: "0.0.99" - source_gen: - dependency: transitive - description: - name: source_gen - url: "https://pub.dartlang.org" - source: hosted - version: "0.9.4+6" - source_span: - dependency: transitive - description: - name: source_span - url: "https://pub.dartlang.org" - source: hosted - version: "1.7.0" - stack_trace: - dependency: transitive - description: - name: stack_trace - url: "https://pub.dartlang.org" - source: hosted - version: "1.9.3" - stream_channel: - dependency: transitive - description: - name: stream_channel - url: "https://pub.dartlang.org" - source: hosted - version: "2.0.0" - stream_transform: - dependency: transitive - description: - name: stream_transform - url: "https://pub.dartlang.org" - source: hosted - version: "0.0.19" - string_scanner: - dependency: transitive - description: - name: string_scanner - url: "https://pub.dartlang.org" - source: hosted - version: "1.0.5" - term_glyph: - dependency: transitive - description: - name: term_glyph - url: "https://pub.dartlang.org" - source: hosted - version: "1.1.0" - test_api: - dependency: transitive - description: - name: test_api - url: "https://pub.dartlang.org" - source: hosted - version: "0.2.16" - timing: - dependency: transitive - description: - name: timing - url: "https://pub.dartlang.org" - source: hosted - version: "0.1.1+2" - typed_data: - dependency: transitive - description: - name: typed_data - url: "https://pub.dartlang.org" - source: hosted - version: "1.1.6" - vector_math: - dependency: transitive - description: - name: vector_math - url: "https://pub.dartlang.org" - source: hosted - version: "2.0.8" - watcher: - dependency: transitive - description: - name: watcher - url: "https://pub.dartlang.org" - source: hosted - version: "0.9.7+12" - web_socket_channel: - dependency: transitive - description: - name: web_socket_channel - url: "https://pub.dartlang.org" - source: hosted - version: "1.1.0" - yaml: - dependency: transitive - description: - name: yaml - url: "https://pub.dartlang.org" - source: hosted - version: "2.2.0" -sdks: - dart: ">=2.7.0 <3.0.0" - flutter: ">=0.1.4 <2.0.0" From f2375993c9c13c22004df3214307adf920f935e2 Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Wed, 17 Jun 2020 10:24:55 +0100 Subject: [PATCH 127/384] Readme --- CHANGELOG.md | 4 + README.md | 250 ++++++++++++++++++++++++++++++++++----------------- 2 files changed, 174 insertions(+), 80 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e7064e3c..ed344c63 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.10.0 + +- Added a way for hooks to potentially abort a widget rebuild. + ## 0.9.0 - Added a `deactivate` life-cycle to `HookState` diff --git a/README.md b/README.md index cd811df4..2a8b5be5 100644 --- a/README.md +++ b/README.md @@ -2,15 +2,49 @@ +# Migration to v0.10.0 + +With the 0.10.0 release, `HookWidget` has been deprecated. + +What was previously written as: + +```dart +class Example extends HookWidget { + const Example({Key key}): super(key: key); + + @override + Widget build(BuildContext context) { + final state = useState(0); + ... + } +} +``` + +Should now be written as: + +```dart +class Example extends StatelessWidget with Hooks { + const Example({Key key}): super(key: key); + + @override + Widget build(BuildContext context) { + final state = useState(0); + ... + } +} +``` + # Flutter Hooks -A flutter implementation of React hooks: https://medium.com/@dan_abramov/making-sense-of-react-hooks-fdbde8803889 +A Flutter implementation of React hooks: https://medium.com/@dan_abramov/making-sense-of-react-hooks-fdbde8803889 -Hooks are a new kind of object that manages a `Widget` life-cycles. They exist for one reason: increase the code-sharing _between_ widgets and as a complete replacement for `StatefulWidget`. +Hooks are a new kind of object that manages a `Widget` life-cycles. They exist +for one reason: increase the code-sharing _between_ widgets by removing duplicates. ## Motivation -`StatefulWidget` suffers from a big problem: it is very difficult to reuse the logic of say `initState` or `dispose`. An obvious example is `AnimationController`: +`StatefulWidget` suffers from a big problem: it is very difficult to reuse the +logic of say `initState` or `dispose`. An obvious example is `AnimationController`: ```dart class Example extends StatefulWidget { @@ -54,25 +88,28 @@ class _ExampleState extends State with SingleTickerProviderStateMixin { } ``` -All widgets that desire to use an `AnimationController` will have to reimplement almost all of this from scratch, which is of course undesired. +All widgets that desire to use an `AnimationController` will have to reimplement +almost all of this from scratch, which is of course undesired. Dart mixins can partially solve this issue, but they suffer from other problems: - A given mixin can only be used once per class. -- Mixins and the class shares the same object. This means that if two mixins define a variable under the same name, the result may vary between compilation fail to unknown behavior. +- Mixins and the class shares the same object.\ + This means that if two mixins define a variable under the same name, the result + may vary between compilation fails to unknown behavior. --- This library proposes a third solution: ```dart -class Example extends HookWidget { - final Duration duration; - +class Example extends StatelessWidget with Hooks { const Example({Key key, @required this.duration}) : assert(duration != null), super(key: key); + final Duration duration; + @override Widget build(BuildContext context) { final controller = useAnimationController(duration: duration); @@ -81,34 +118,44 @@ class Example extends HookWidget { } ``` -This code is strictly equivalent to the previous example. It still disposes the `AnimationController` and still updates its `duration` when `Example.duration` changes. +This code is strictly equivalent to the previous example. It still disposes the +`AnimationController` and still updates its `duration` when `Example.duration` changes. But you're probably thinking: > Where did all the logic go? -That logic moved into `useAnimationController`, a function included directly in this library (see [Existing hooks](https://github.com/rrousselGit/flutter_hooks#existing-hooks)). It is what we call a _Hook_. +That logic moved into `useAnimationController`, a function included directly in +this library (see [Existing hooks](https://github.com/rrousselGit/flutter_hooks#existing-hooks)). +It is what we call a _Hook_. Hooks are a new kind of objects with some specificities: -- They can only be used in the `build` method of a `HookWidget`. +- They can only be used in the `build` method of a widget that mix-in `Hooks`. - The same hook is reusable an infinite number of times - The following code defines two independent `AnimationController`, and they are correctly preserved when the widget rebuild. + The following code defines two independent `AnimationController`, and they are + correctly preserved when the widget rebuild. -```dart -Widget build(BuildContext context) { - final controller = useAnimationController(); - final controller2 = useAnimationController(); - return Container(); -} -``` + ```dart + Widget build(BuildContext context) { + final controller = useAnimationController(); + final controller2 = useAnimationController(); + return Container(); + } + ``` -- Hooks are entirely independent of each other and from the widget. This means they can easily be extracted into a package and published on [pub](https://pub.dartlang.org/) for others to use. +- Hooks are entirely independent of each other and from the widget.\ + This means they can easily be extracted into a package and published on + [pub](https://pub.dartlang.org/) for others to use. ## Principle -Similarly to `State`, hooks are stored on the `Element` of a `Widget`. But instead of having one `State`, the `Element` stores a `List`. Then to use a `Hook`, one must call `Hook.use`. +Similarly to `State`, hooks are stored on the `Element` of a `Widget`. But instead +of having one `State`, the `Element` stores a `List`. Then to use a `Hook`, +one must call `Hook.use`. -The hook returned by `use` is based on the number of times it has been called. The first call returns the first hook; the second call returns the second hook, the third returns the third hook, ... +The hook returned by `use` is based on the number of times it has been called. +The first call returns the first hook; the second call returns the second hook, +the third returns the third hook, ... If this is still unclear, a naive implementation of hooks is the following: @@ -127,17 +174,31 @@ class HookElement extends Element { } ``` -For more explanation of how they are implemented, here's a great article about how they did it in React: https://medium.com/@ryardley/react-hooks-not-magic-just-arrays-cd4f1857236e +For more explanation of how they are implemented, here's a great article about +how they did it in React: https://medium.com/@ryardley/react-hooks-not-magic-just-arrays-cd4f1857236e ## Rules Due to hooks being obtained from their index, some rules must be respected: -### DO call `use` unconditionally +### DO always prefer your hooks with `use`: + +```dart +Widget build(BuildContext context) { + // starts with `use`, good name + useMyHook(); + // doesn't that with `use`, could confuse people into thinking that this isn't a hook + myHook(); + // .... +} +``` + + +### DO call hooks unconditionally ```dart Widget build(BuildContext context) { - Hook.use(MyHook()); + useMyHook(); // .... } ``` @@ -147,7 +208,7 @@ Widget build(BuildContext context) { ```dart Widget build(BuildContext context) { if (condition) { - Hook.use(MyHook()); + useMyHook(); } // .... } @@ -159,8 +220,8 @@ Widget build(BuildContext context) { ```dart Widget build(BuildContext context) { - Hook.use(Hook1()); - Hook.use(Hook2()); + useMyHook(); + useAnotherHook(); // .... } ``` @@ -169,11 +230,11 @@ Widget build(BuildContext context) { ```dart Widget build(BuildContext context) { - Hook.use(Hook1()); + useMyHook(); if (condition) { return Container(); } - Hook.use(Hook2()); + useAnotherHook(); // .... } ``` @@ -189,17 +250,17 @@ But worry not, `HookWidget` overrides the default hot-reload behavior to work wi Consider the following list of hooks: ```dart -Hook.use(HookA()); -Hook.use(HookB(0)); -Hook.use(HookC(0)); +useA(); +useB(0); +useC(); ``` Then consider that after a hot-reload, we edited the parameter of `HookB`: ```dart -Hook.use(HookA()); -Hook.use(HookB(42)); -Hook.use(HookC()); +useA(); +useB(42); +useC(); ``` Here everything works fine; all hooks keep their states. @@ -207,8 +268,8 @@ Here everything works fine; all hooks keep their states. Now consider that we removed `HookB`. We now have: ```dart -Hook.use(HookA()); -Hook.use(HookC()); +useA(); +useC(); ``` In this situation, `HookA` keeps its state but `HookC` gets a hard reset. @@ -221,62 +282,65 @@ There are two ways to create a hook: - A function -Functions are by far the most common way to write a hook. Thanks to hooks being composable by nature, a function will be able to combine other hooks to create a custom hook. By convention, these functions will be prefixed by `use`. + Functions are by far the most common way to write a hook. Thanks to hooks being + composable by nature, a function will be able to combine other hooks to create + a custom hook. By convention, these functions will be prefixed by `use`. -The following defines a custom hook that creates a variable and logs its value on the console whenever the value changes: + The following defines a custom hook that creates a variable and logs its value + on the console whenever the value changes: -```dart -ValueNotifier useLoggedState(BuildContext context, [T initialData]) { - final result = useState(initialData); - useValueChanged(result.value, (_, __) { - print(result.value); - }); - return result; -} -``` + ```dart + ValueNotifier useLoggedState(BuildContext context, [T initialData]) { + final result = useState(initialData); + useValueChanged(result.value, (_, __) { + print(result.value); + }); + return result; + } + ``` - A class -When a hook becomes too complex, it is possible to convert it into a class that extends `Hook`, which can then be used using `Hook.use`. As a class, the hook will look very similar to a `State` and have access to life-cycles and methods such as `initHook`, `dispose` and `setState`. It is usually a good practice to hide the class under a function as such: + When a hook becomes too complex, it is possible to convert it into a class that extends `Hook`, which can then be used using `Hook.use`.\ + As a class, the hook will look very similar to a `State` and have access to + life-cycles and methods such as `initHook`, `dispose` and `setState` + It is usually a good practice to hide the class under a function as such: -```dart -Result useMyHook(BuildContext context) { - return Hook.use(_MyHook()); -} -``` + ```dart + Result useMyHook(BuildContext context) { + return Hook.use(const _TimeAlive()); + } + ``` -The following defines a hook that prints the time a `State` has been alive. + The following defines a hook that prints the time a `State` has been alive. -```dart -class _TimeAlive extends Hook { - const _TimeAlive(); + ```dart + class _TimeAlive extends Hook { + const _TimeAlive(); - @override - _TimeAliveState createState() => _TimeAliveState(); -} + @override + _TimeAliveState createState() => _TimeAliveState(); + } -class _TimeAliveState extends HookState> { - DateTime start; + class _TimeAliveState extends HookState { + DateTime start; - @override - void initHook() { - super.initHook(); - start = DateTime.now(); - } + @override + void initHook() { + super.initHook(); + start = DateTime.now(); + } - @override - void build(BuildContext context) { - // this hook doesn't create anything nor uses other hooks - } + @override + void build(BuildContext context) {} - @override - void dispose() { - print(DateTime.now().difference(start)); - super.dispose(); + @override + void dispose() { + print(DateTime.now().difference(start)); + super.dispose(); + } } -} - -``` + ``` ## Existing hooks @@ -335,3 +399,29 @@ A series of hooks with no particular theme. | [usePrevious](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/usePrevious.html) | Returns the previous argument called to [usePrevious]. | | [useTextEditingController](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useTextEditingController.html) | Create a `TextEditingController` | | [useFocusNode](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useFocusNode.html) | Create a `FocusNode` | + + +## Contributions + +Contributions are welcomed! + +If you feel that a hook is missing, feel free to open a pull-request. + +For a custom-hook to be merged, you will need to do the following: + +- Describe the use-case. + + Open an issue explaining why we need this hook, how to use it, ... + This is important as a hook will not get merged if the hook doens't appeal to + a large number of people. + + If your hook is rejected, don't worry! A rejection doesn't mean that it won't + be merged later in the future if more people shows an interest in it. + In the mean-time, feel free to publish your hook as a package on https://pub.dev. + +- Write tests for your hook + + A hook will not be merged unles fully tested, to avoid breaking it inadvertendly + in the future. + +- Add it to the Readme & write documentation for it. \ No newline at end of file From 8a09a120f06bdbf637890c45190f88f4179a3bd9 Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Wed, 17 Jun 2020 11:27:35 +0100 Subject: [PATCH 128/384] Added StatefulHookWidget --- CHANGELOG.md | 1 + README.md | 34 +---------- lib/src/framework.dart | 117 ++++++++++++++++++++----------------- test/hook_widget_test.dart | 52 +++++++++++++++++ 4 files changed, 118 insertions(+), 86 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ed344c63..9c8d9ec3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ ## 0.10.0 - Added a way for hooks to potentially abort a widget rebuild. +- Added `StatefulHookWidget`, a `StatefulWidget` that can use hooks inside its `build` method. ## 0.9.0 diff --git a/README.md b/README.md index 2a8b5be5..2b27092b 100644 --- a/README.md +++ b/README.md @@ -2,38 +2,6 @@ -# Migration to v0.10.0 - -With the 0.10.0 release, `HookWidget` has been deprecated. - -What was previously written as: - -```dart -class Example extends HookWidget { - const Example({Key key}): super(key: key); - - @override - Widget build(BuildContext context) { - final state = useState(0); - ... - } -} -``` - -Should now be written as: - -```dart -class Example extends StatelessWidget with Hooks { - const Example({Key key}): super(key: key); - - @override - Widget build(BuildContext context) { - final state = useState(0); - ... - } -} -``` - # Flutter Hooks A Flutter implementation of React hooks: https://medium.com/@dan_abramov/making-sense-of-react-hooks-fdbde8803889 @@ -103,7 +71,7 @@ Dart mixins can partially solve this issue, but they suffer from other problems: This library proposes a third solution: ```dart -class Example extends StatelessWidget with Hooks { +class Example extends HookWidget { const Example({Key key, @required this.duration}) : assert(duration != null), super(key: key); diff --git a/lib/src/framework.dart b/lib/src/framework.dart index 3d68c216..9cb3c2d5 100644 --- a/lib/src/framework.dart +++ b/lib/src/framework.dart @@ -124,14 +124,14 @@ abstract class Hook { /// Register a [Hook] and returns its value /// - /// [use] must be called withing [HookWidget.build] and - /// all calls to [use] must be made unconditionally, always - /// on the same order. + /// [use] must be called withing `build` of either [HookWidget] or [StatefulHookWidget], + /// and all calls to [use] must be made unconditionally, always on the same order. /// /// See [Hook] for more explanations. static R use(Hook hook) { assert(HookElement._currentContext != null, ''' -`Hook.use` can only be called from the build method of HookWidget. +Hooks can only be called from the build method of a widget that mix-in `Hooks`. + Hooks should only be called within the build method of a widget. Calling them outside of build method leads to an unstable state and is therefore prohibited. '''); @@ -211,7 +211,7 @@ abstract class HookState> { @protected void dispose() {} - /// Called synchronously after the [HookWidget.build] method finished + /// Called synchronously after the `build` method finished @protected void didBuild() {} @@ -256,7 +256,7 @@ abstract class HookState> { /// Mark the associated [HookWidget] as **potentially** needing to rebuild. /// /// As opposed to [setState], the rebuild is optional and can be cancelled right - /// before [HookWidget.build] is called, by having [shouldRebuild] return false. + /// before `build` is called, by having [shouldRebuild] return false. void markMayNeedRebuild() { if (_element._isOptionalRebuild != false) { _element @@ -283,9 +283,8 @@ class _Entry extends LinkedListEntry<_Entry> { } /// An [Element] that uses a [HookWidget] as its configuration. -class HookElement extends StatefulElement { - /// Creates an element that uses the given widget as its configuration. - HookElement(HookWidget widget) : super(widget); +@visibleForTesting +mixin HookElement on ComponentElement { static HookElement _currentContext; Iterator _currentHook; @@ -301,7 +300,7 @@ class HookElement extends StatefulElement { bool _debugIsInitHook; @override - void update(StatefulWidget newWidget) { + void update(Widget newWidget) { _isOptionalRebuild = false; super.update(newWidget); } @@ -312,9 +311,6 @@ class HookElement extends StatefulElement { super.didChangeDependencies(); } - @override - HookWidget get widget => super.widget as HookWidget; - @override Widget build() { final mustRebuild = _isOptionalRebuild != true || @@ -394,13 +390,16 @@ This may happen if the call to `Hook.use` is made under some condition. try { hook.didBuild(); } catch (exception, stack) { - FlutterError.reportError(FlutterErrorDetails( - exception: exception, - stack: stack, - library: 'hooks library', - context: DiagnosticsNode.message( - 'while calling `didBuild` on ${hook.runtimeType}'), - )); + FlutterError.reportError( + FlutterErrorDetails( + exception: exception, + stack: stack, + library: 'hooks library', + context: DiagnosticsNode.message( + 'while calling `didBuild` on ${hook.runtimeType}', + ), + ), + ); } } } @@ -415,13 +414,16 @@ This may happen if the call to `Hook.use` is made under some condition. try { hook.dispose(); } catch (exception, stack) { - FlutterError.reportError(FlutterErrorDetails( - exception: exception, - stack: stack, - library: 'hooks library', - context: - DiagnosticsNode.message('while disposing ${hook.runtimeType}'), - )); + FlutterError.reportError( + FlutterErrorDetails( + exception: exception, + stack: stack, + library: 'hooks library', + context: DiagnosticsNode.message( + 'while disposing ${hook.runtimeType}', + ), + ), + ); } } } @@ -434,14 +436,16 @@ This may happen if the call to `Hook.use` is made under some condition. try { hook.deactivate(); } catch (exception, stack) { - FlutterError.reportError(FlutterErrorDetails( - exception: exception, - stack: stack, - library: 'hooks library', - context: DiagnosticsNode.message( - 'while deactivating ${hook.runtimeType}', + FlutterError.reportError( + FlutterErrorDetails( + exception: exception, + stack: stack, + library: 'hooks library', + context: DiagnosticsNode.message( + 'while deactivating ${hook.runtimeType}', + ), ), - )); + ); } } } @@ -466,8 +470,10 @@ This may happen if the call to `Hook.use` is made under some condition. HookState> hookState; // first build if (_currentHook == null) { - assert(_debugDidReassemble || !_didFinishBuildOnce, - 'No previous hook found at $_hookIndex, is a hook wrapped in a `if`?'); + assert( + _debugDidReassemble || !_didFinishBuildOnce, + 'No previous hook found at $_hookIndex, is a hook wrapped in a `if`?', + ); hookState = _createHookState(hook); _hooks ??= []; _hooks.add(hookState); @@ -566,6 +572,7 @@ This may happen if the call to `Hook.use` is made under some condition. _debugIsInitHook = true; return true; }(), ''); + final state = hook.createState() .._element = this .._hook = hook @@ -588,30 +595,34 @@ This may happen if the call to `Hook.use` is made under some condition. /// /// The difference is that it can use [Hook], which allows /// [HookWidget] to store mutable data without implementing a [State]. -abstract class HookWidget extends StatefulWidget { +abstract class HookWidget extends StatelessWidget { /// Initializes [key] for subclasses. const HookWidget({Key key}) : super(key: key); @override - HookElement createElement() => HookElement(this); - - @override - _HookWidgetState createState() => _HookWidgetState(); + _StatelessHookElement createElement() => _StatelessHookElement(this); +} - /// Describes the part of the user interface represented by this widget. - /// - /// See also: - /// - /// * [StatelessWidget.build] - @protected - Widget build(BuildContext context); +class _StatelessHookElement extends StatelessElement with HookElement { + _StatelessHookElement(HookWidget hooks) : super(hooks); } -class _HookWidgetState extends State { +/// A [StatefulWidget] that can use [Hook] +/// +/// It's usage is very similar to [StatefulWidget], but use hooks inside [State.build]. +/// +/// The difference is that it can use [Hook], which allows +/// [HookWidget] to store mutable data without implementing a [State]. +abstract class StatefulHookWidget extends StatefulWidget { + /// Initializes [key] for subclasses. + const StatefulHookWidget({Key key}) : super(key: key); + @override - Widget build(BuildContext context) { - return widget.build(context); - } + _StatefulHookElement createElement() => _StatefulHookElement(this); +} + +class _StatefulHookElement extends StatefulElement with HookElement { + _StatefulHookElement(StatefulHookWidget hooks) : super(hooks); } /// Obtain the [BuildContext] of the building [HookWidget]. @@ -621,7 +632,7 @@ BuildContext useContext() { return HookElement._currentContext; } -/// A [HookWidget] that defer its [HookWidget.build] to a callback +/// A [HookWidget] that defer its `build` to a callback class HookBuilder extends HookWidget { /// Creates a widget that delegates its build to a callback. /// diff --git a/test/hook_widget_test.dart b/test/hook_widget_test.dart index 71cff077..a00d636d 100644 --- a/test/hook_widget_test.dart +++ b/test/hook_widget_test.dart @@ -60,6 +60,23 @@ void main() { reset(reassemble); }); + testWidgets('StatefulHookWidget', (tester) async { + final notifier = ValueNotifier(0); + + await tester.pumpWidget(MyStatefulHook(value: 0, notifier: notifier)); + + expect(find.text('0 0'), findsOneWidget); + + await tester.pumpWidget(MyStatefulHook(value: 1, notifier: notifier)); + + expect(find.text('1 0'), findsOneWidget); + + notifier.value++; + await tester.pump(); + + expect(find.text('1 1'), findsOneWidget); + }); + testWidgets( 'should call deactivate when removed from and inserted into another place', (tester) async { @@ -1170,3 +1187,38 @@ class MyHookState extends HookState { return this; } } + +class MyStatefulHook extends StatefulHookWidget { + const MyStatefulHook({Key key, this.value, this.notifier}) : super(key: key); + + final int value; + final ValueNotifier notifier; + + @override + _MyStatefulHookState createState() => _MyStatefulHookState(); +} + +class _MyStatefulHookState extends State { + int value; + + @override + void initState() { + super.initState(); + // voluntarily ues widget.value to verify that state life-cycles are called + value = widget.value; + } + + @override + void didUpdateWidget(MyStatefulHook oldWidget) { + super.didUpdateWidget(oldWidget); + value = widget.value; + } + + @override + Widget build(BuildContext context) { + return Text( + '$value ${useValueListenable(widget.notifier)}', + textDirection: TextDirection.ltr, + ); + } +} From 87830888315b091ff9995e9ccf2b3423850db7e0 Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Wed, 17 Jun 2020 11:36:20 +0100 Subject: [PATCH 129/384] Dartdoc --- lib/src/misc.dart | 2 +- lib/src/primitives.dart | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/src/misc.dart b/lib/src/misc.dart index c7d6d3ac..ea293f55 100644 --- a/lib/src/misc.dart +++ b/lib/src/misc.dart @@ -10,7 +10,7 @@ abstract class Store { /// Dispatches an action. /// /// Actions are dispatched synchronously. - /// It is impossible to try to dispatch actions during [HookWidget.build]. + /// It is impossible to try to dispatch actions during `build`. void dispatch(Action action); } diff --git a/lib/src/primitives.dart b/lib/src/primitives.dart index 3264f665..6ea73a60 100644 --- a/lib/src/primitives.dart +++ b/lib/src/primitives.dart @@ -100,7 +100,7 @@ class _ValueChangedHookState /// Useful for side-effects and optionally canceling them. /// -/// [useEffect] is called synchronously on every [HookWidget.build], unless +/// [useEffect] is called synchronously on every `build`, unless typedef Dispose = void Function(); /// [keys] is specified. In which case [useEffect] is called again only if @@ -109,7 +109,7 @@ typedef Dispose = void Function(); /// It takes an [effect] callback and calls it synchronously. /// That [effect] may optionally return a function, which will be called when the [effect] is called again or if the widget is disposed. /// -/// By default [effect] is called on every [HookWidget.build] call, unless [keys] is specified. +/// By default [effect] is called on every `build` call, unless [keys] is specified. /// In which case, [effect] is called once on the first [useEffect] call and whenever something within [keys] change/ /// /// The following example call [useEffect] to subscribes to a [Stream] and cancel the subscription when the widget is disposed. From a706e9e5c4240125f573b7262dbf31620077b152 Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Sat, 20 Jun 2020 01:46:51 +0100 Subject: [PATCH 130/384] closes #81 --- CHANGELOG.md | 29 +++++++++++++++++++++ lib/src/animation.dart | 53 +++++++++++++++++++++----------------- lib/src/framework.dart | 2 +- test/hook_widget_test.dart | 31 +++++++++++++++++++++- 4 files changed, 89 insertions(+), 26 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9c8d9ec3..9a7dcb21 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,34 @@ ## 0.10.0 +**Breaking change**: + +- The order in which hooks are disposed has been reversed. + + Consider: + + ```dart + useSomething(); + useSomethingElse(); + ``` + + Before, the `useSomething` was disposed before `useSomethingElse`. + Now, `useSomethingElse` is disposed before the `useSomething`. + + The reason for this change is for cases like: + + ```dart + // Creates an AnimationController + final animationController = useAnimationController(); + // Immediatly listen to the AnimationController + useListenable(animationController); + ``` + + Before, when the widget was disposed, this caused an exception as + `useListenable` unsubscribed to the `AnimationController` _after_ its `dispose` + method was called. + +**Non-breaking changes**: + - Added a way for hooks to potentially abort a widget rebuild. - Added `StatefulHookWidget`, a `StatefulWidget` that can use hooks inside its `build` method. diff --git a/lib/src/animation.dart b/lib/src/animation.dart index 8be494b6..1cad8170 100644 --- a/lib/src/animation.dart +++ b/lib/src/animation.dart @@ -33,16 +33,20 @@ AnimationController useAnimationController({ AnimationBehavior animationBehavior = AnimationBehavior.normal, List keys, }) { - return Hook.use(_AnimationControllerHook( - duration: duration, - debugLabel: debugLabel, - initialValue: initialValue, - lowerBound: lowerBound, - upperBound: upperBound, - vsync: vsync, - animationBehavior: animationBehavior, - keys: keys, - )); + vsync ??= useSingleTickerProvider(keys: keys); + + return Hook.use( + _AnimationControllerHook( + duration: duration, + debugLabel: debugLabel, + initialValue: initialValue, + lowerBound: lowerBound, + upperBound: upperBound, + vsync: vsync, + animationBehavior: animationBehavior, + keys: keys, + ), + ); } class _AnimationControllerHook extends Hook { @@ -74,13 +78,24 @@ class _AnimationControllerHookState extends HookState { AnimationController _animationController; + @override + void initHook() { + super.initHook(); + _animationController = AnimationController( + vsync: hook.vsync, + duration: hook.duration, + debugLabel: hook.debugLabel, + lowerBound: hook.lowerBound, + upperBound: hook.upperBound, + animationBehavior: hook.animationBehavior, + value: hook.initialValue, + ); + } + @override void didUpdateHook(_AnimationControllerHook oldHook) { super.didUpdateHook(oldHook); if (hook.vsync != oldHook.vsync) { - assert(hook.vsync != null && oldHook.vsync != null, ''' -Switching between controller and uncontrolled vsync is not allowed. -'''); _animationController.resync(hook.vsync); } @@ -91,17 +106,7 @@ Switching between controller and uncontrolled vsync is not allowed. @override AnimationController build(BuildContext context) { - final vsync = hook.vsync ?? useSingleTickerProvider(keys: hook.keys); - - return _animationController ??= AnimationController( - vsync: vsync, - duration: hook.duration, - debugLabel: hook.debugLabel, - lowerBound: hook.lowerBound, - upperBound: hook.upperBound, - animationBehavior: hook.animationBehavior, - value: hook.initialValue, - ); + return _animationController; } @override diff --git a/lib/src/framework.dart b/lib/src/framework.dart index 9cb3c2d5..3a479e5a 100644 --- a/lib/src/framework.dart +++ b/lib/src/framework.dart @@ -410,7 +410,7 @@ This may happen if the call to `Hook.use` is made under some condition. void unmount() { super.unmount(); if (_hooks != null) { - for (final hook in _hooks) { + for (final hook in _hooks.reversed) { try { hook.dispose(); } catch (exception, stack) { diff --git a/test/hook_widget_test.dart b/test/hook_widget_test.dart index a00d636d..8d9b3c55 100644 --- a/test/hook_widget_test.dart +++ b/test/hook_widget_test.dart @@ -60,6 +60,31 @@ void main() { reset(reassemble); }); + testWidgets('hooks are disposed in reverse order on unmount', (tester) async { + final first = MockDispose(); + final second = MockDispose(); + + await tester.pumpWidget( + HookBuilder(builder: (c) { + useEffect(() => first); + useEffect(() => second); + return Container(); + }), + ); + + verifyNoMoreInteractions(first); + verifyNoMoreInteractions(second); + + await tester.pumpWidget(Container()); + + verifyInOrder([ + second(), + first(), + ]); + verifyNoMoreInteractions(first); + verifyNoMoreInteractions(second); + }); + testWidgets('StatefulHookWidget', (tester) async { final notifier = ValueNotifier(0); @@ -649,8 +674,8 @@ void main() { expect(tester.takeException(), 24); verifyInOrder([ - dispose.call(), dispose2.call(), + dispose.call(), ]); }); @@ -1176,6 +1201,10 @@ void main() { }); } +class MockDispose extends Mock { + void call(); +} + class MyHook extends Hook { @override MyHookState createState() => MyHookState(); From 32734815c5b171d5e9e3559a5613a132ab2c2778 Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Sat, 20 Jun 2020 01:48:17 +0100 Subject: [PATCH 131/384] v0.10.0-dev+2 --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index 6ab77824..9ab5cb83 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -3,7 +3,7 @@ description: A flutter implementation of React hooks. It adds a new kind of widg homepage: https://github.com/rrousselGit/flutter_hooks author: Remi Rousselet -version: 0.10.0-dev+1 +version: 0.10.0-dev+2 environment: sdk: ">=2.7.0 <3.0.0" From c5ff3cef02317c9641abc29497d02bd75be677f0 Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Tue, 23 Jun 2020 03:24:23 +0100 Subject: [PATCH 132/384] v0.10.0 --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index 9ab5cb83..d172ecbe 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -3,7 +3,7 @@ description: A flutter implementation of React hooks. It adds a new kind of widg homepage: https://github.com/rrousselGit/flutter_hooks author: Remi Rousselet -version: 0.10.0-dev+2 +version: 0.10.0 environment: sdk: ">=2.7.0 <3.0.0" From e9bcf72792918726ddb05c01aae988fe5ab3b5d7 Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Sat, 4 Jul 2020 11:09:58 +0100 Subject: [PATCH 133/384] fixes #149 --- CHANGELOG.md | 4 +++ example/.flutter-plugins-dependencies | 2 +- lib/src/framework.dart | 26 +++++++++++++--- test/hook_widget_test.dart | 44 +++++++++++++++++++++++++-- test/use_effect_test.dart | 2 +- 5 files changed, 69 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9a7dcb21..2e9d68c5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.10.1 + +- Fix a bug where the order in which hooks are disposed is incorrect. + ## 0.10.0 **Breaking change**: diff --git a/example/.flutter-plugins-dependencies b/example/.flutter-plugins-dependencies index 1296f2cc..982acf6a 100644 --- a/example/.flutter-plugins-dependencies +++ b/example/.flutter-plugins-dependencies @@ -1 +1 @@ -{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"shared_preferences","path":"/Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/shared_preferences-0.4.3/","dependencies":[]}],"android":[{"name":"shared_preferences","path":"/Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/shared_preferences-0.4.3/","dependencies":[]}],"macos":[],"linux":[],"windows":[],"web":[]},"dependencyGraph":[{"name":"shared_preferences","dependencies":[]}],"date_created":"2020-06-17 09:08:41.153385","version":"1.20.0-0.0.pre"} \ No newline at end of file +{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"shared_preferences","path":"/Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/shared_preferences-0.4.3/","dependencies":[]}],"android":[{"name":"shared_preferences","path":"/Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/shared_preferences-0.4.3/","dependencies":[]}],"macos":[],"linux":[],"windows":[],"web":[]},"dependencyGraph":[{"name":"shared_preferences","dependencies":[]}],"date_created":"2020-07-02 11:47:33.991520","version":"1.20.0-3.0.pre.100"} \ No newline at end of file diff --git a/lib/src/framework.dart b/lib/src/framework.dart index 3a479e5a..31f2f77d 100644 --- a/lib/src/framework.dart +++ b/lib/src/framework.dart @@ -291,6 +291,7 @@ mixin HookElement on ComponentElement { int _hookIndex; List _hooks; LinkedList<_Entry> _shouldRebuildQueue; + LinkedList<_Entry> _needDispose; bool _isOptionalRebuild = false; Widget _buildCache; bool _didFinishBuildOnce = false; @@ -334,8 +335,19 @@ mixin HookElement on ComponentElement { return true; }(), ''); HookElement._currentContext = this; - _buildCache = super.build(); - HookElement._currentContext = null; + try { + _buildCache = super.build(); + } finally { + HookElement._currentContext = null; + if (_needDispose != null) { + for (var toDispose = _needDispose.last; + toDispose != null; + toDispose = toDispose.previous) { + toDispose.value.dispose(); + } + _needDispose = null; + } + } // dispose removed items assert(() { @@ -507,8 +519,10 @@ This may happen if the call to `Hook.use` is made under some condition. hookState = _pushHook(hook); _currentHook.moveNext(); } else { - assert(_currentHook.current != null, - 'No previous hook found at $_hookIndex, is a hook wrapped in a `if`?'); + assert( + _currentHook.current != null, + 'No previous hook found at $_hookIndex, is a hook wrapped in a `if`?', + ); assert(_debugTypesAreRight(hook), ''); if (_currentHook.current.hook == hook) { @@ -533,7 +547,9 @@ This may happen if the call to `Hook.use` is made under some condition. } HookState> _replaceHookAt(int index, Hook hook) { - _hooks.removeAt(_hookIndex).dispose(); + _needDispose ??= LinkedList(); + _needDispose.add(_Entry(_hooks.removeAt(_hookIndex))); + // _hooks.removeAt(_hookIndex).dispose(); final hookState = _createHookState(hook); _hooks.insert(_hookIndex, hookState); return hookState; diff --git a/test/hook_widget_test.dart b/test/hook_widget_test.dart index 8d9b3c55..82d3cae8 100644 --- a/test/hook_widget_test.dart +++ b/test/hook_widget_test.dart @@ -60,6 +60,46 @@ void main() { reset(reassemble); }); + testWidgets('hooks are disposed in reverse order when their keys changes', + (tester) async { + final first = MockDispose(); + final second = MockDispose(); + + await tester.pumpWidget( + HookBuilder(builder: (c) { + useEffect(() => first, [0]); + useEffect(() => second, [0]); + return Container(); + }), + ); + + verifyZeroInteractions(first); + verifyZeroInteractions(second); + + await tester.pumpWidget( + HookBuilder(builder: (c) { + useEffect(() => first, [1]); + useEffect(() => second, [1]); + return Container(); + }), + ); + + verifyInOrder([ + second(), + first(), + ]); + verifyNoMoreInteractions(first); + verifyNoMoreInteractions(second); + + await tester.pumpWidget(Container()); + + verifyInOrder([ + second(), + first(), + ]); + verifyNoMoreInteractions(first); + verifyNoMoreInteractions(second); + }); testWidgets('hooks are disposed in reverse order on unmount', (tester) async { final first = MockDispose(); final second = MockDispose(); @@ -452,10 +492,10 @@ void main() { await tester.pumpWidget(HookBuilder(builder: builder.call)); verifyInOrder([ - dispose.call(), createState.call(), initHook.call(), build.call(context), + dispose.call(), didBuild.call(), ]); verifyNoMoreHookInteration(); @@ -490,10 +530,10 @@ void main() { await tester.pumpWidget(HookBuilder(builder: builder.call)); verifyInOrder([ - dispose.call(), createState.call(), initHook.call(), build.call(context), + dispose.call(), didBuild.call() ]); verifyNoMoreHookInteration(); diff --git a/test/use_effect_test.dart b/test/use_effect_test.dart index d9578601..670ecc68 100644 --- a/test/use_effect_test.dart +++ b/test/use_effect_test.dart @@ -211,8 +211,8 @@ void main() { await tester.pumpWidget(builder()); verifyInOrder([ - disposerA.call(), effect.call(), + disposerA.call(), ]); verifyNoMoreInteractions(disposerA); verifyNoMoreInteractions(effect); From edc3dde7934e98ca2efc1aed68f72ed3f63429a9 Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Sat, 4 Jul 2020 19:23:22 +0100 Subject: [PATCH 134/384] Refactor internals & allow early returns --- CHANGELOG.md | 22 +- README.md | 29 +- lib/src/framework.dart | 351 +++---- test/hook_builder_test.dart | 28 +- test/hook_widget_test.dart | 1169 +++++++++++------------ test/memoized_test.dart | 210 ++-- test/mock.dart | 86 +- test/use_animation_controller_test.dart | 41 +- test/use_animation_test.dart | 16 +- test/use_effect_test.dart | 114 ++- test/use_listenable_test.dart | 18 +- test/use_reassemble_test.dart | 12 +- test/use_reducer_test.dart | 80 +- test/use_ticker_provider_test.dart | 8 +- test/use_value_changed_test.dart | 29 +- test/use_value_listenable_test.dart | 18 +- test/use_value_notifier_test.dart | 16 +- 17 files changed, 1093 insertions(+), 1154 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2e9d68c5..c44a5491 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,26 @@ -## 0.10.1 +## 0.11.0 + +**Breaking change**: + +- Removed `HookState.didBuild`. + If you still need it, use `addPostFrameCallback` or `Future.microtask`. + +**Non-breaking changes**: - Fix a bug where the order in which hooks are disposed is incorrect. +- It is now allowed to rebuild a `HookWidget` with more/less hooks than previously. + Example: + + ```dart + Widget build(context) { + useSomething(); + if (condition) { + return Container(); + } + useSomething() + return Container(); + } + ``` ## 0.10.0 diff --git a/README.md b/README.md index 2b27092b..32e69e0b 100644 --- a/README.md +++ b/README.md @@ -161,7 +161,6 @@ Widget build(BuildContext context) { } ``` - ### DO call hooks unconditionally ```dart @@ -184,31 +183,6 @@ Widget build(BuildContext context) { --- -### DO always call all the hooks: - -```dart -Widget build(BuildContext context) { - useMyHook(); - useAnotherHook(); - // .... -} -``` - -### DON'T aborts `build` method before all hooks have been called: - -```dart -Widget build(BuildContext context) { - useMyHook(); - if (condition) { - return Container(); - } - useAnotherHook(); - // .... -} -``` - ---- - ### About hot-reload Since hooks are obtained from their index, one may think that hot-reload while refactoring will break the application. @@ -368,7 +342,6 @@ A series of hooks with no particular theme. | [useTextEditingController](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useTextEditingController.html) | Create a `TextEditingController` | | [useFocusNode](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useFocusNode.html) | Create a `FocusNode` | - ## Contributions Contributions are welcomed! @@ -392,4 +365,4 @@ For a custom-hook to be merged, you will need to do the following: A hook will not be merged unles fully tested, to avoid breaking it inadvertendly in the future. -- Add it to the Readme & write documentation for it. \ No newline at end of file +- Add it to the Readme & write documentation for it. diff --git a/lib/src/framework.dart b/lib/src/framework.dart index 31f2f77d..7b945884 100644 --- a/lib/src/framework.dart +++ b/lib/src/framework.dart @@ -9,6 +9,22 @@ import 'package:flutter/widgets.dart'; /// `true` by default. It has no impact on release builds. bool debugHotReloadHooksEnabled = true; +/// Register a [Hook] and returns its value +/// +/// [use] must be called withing `build` of either [HookWidget] or [StatefulHookWidget], +/// and all calls to [use] must be made unconditionally, always on the same order. +/// +/// See [Hook] for more explanations. +R use(Hook hook) { + assert(HookElement._currentHookElement != null, ''' +Hooks can only be called from the build method of a widget that mix-in `Hooks`. + +Hooks should only be called within the build method of a widget. +Calling them outside of build method leads to an unstable state and is therefore prohibited. +'''); + return HookElement._currentHookElement._use(hook); +} + /// [Hook] is similar to a [StatelessWidget], but is not associated /// to an [Element]. /// @@ -128,14 +144,15 @@ abstract class Hook { /// and all calls to [use] must be made unconditionally, always on the same order. /// /// See [Hook] for more explanations. + @Deprecated('Use `use` instead of `Hook.use`') static R use(Hook hook) { - assert(HookElement._currentContext != null, ''' + assert(HookElement._currentHookElement != null, ''' Hooks can only be called from the build method of a widget that mix-in `Hooks`. Hooks should only be called within the build method of a widget. Calling them outside of build method leads to an unstable state and is therefore prohibited. '''); - return HookElement._currentContext._use(hook); + return HookElement._currentHookElement._use(hook); } /// A list of objects that specify if a [HookState] should be reused or a new one should be created. @@ -211,10 +228,6 @@ abstract class HookState> { @protected void dispose() {} - /// Called synchronously after the `build` method finished - @protected - void didBuild() {} - /// Called everytimes the [HookState] is requested /// /// [build] is where an [HookState] may use other hooks. This restriction is made to ensure that hooks are unconditionally always requested @@ -279,26 +292,75 @@ abstract class HookState> { class _Entry extends LinkedListEntry<_Entry> { _Entry(this.value); - final T value; + T value; +} + +extension on HookElement { + HookState> _createHookState(Hook hook) { + assert(() { + _debugIsInitHook = true; + return true; + }(), ''); + + final state = hook.createState() + .._element = this + .._hook = hook + ..initHook(); + + assert(() { + _debugIsInitHook = false; + return true; + }(), ''); + + return state; + } + + void _appendHook(Hook hook) { + final result = _createHookState(hook); + _currentHookState = _Entry(result); + _hooks.add(_currentHookState); + } + + void _unmountAllRemainingHooks() { + if (_currentHookState != null) { + _needDispose ??= LinkedList(); + // Mark all hooks >= this one as needing dispose + while (_currentHookState != null) { + final previousHookState = _currentHookState; + _currentHookState = _currentHookState.next; + previousHookState.unlink(); + _needDispose.add(previousHookState); + } + } + } } /// An [Element] that uses a [HookWidget] as its configuration. @visibleForTesting mixin HookElement on ComponentElement { - static HookElement _currentContext; + static HookElement _currentHookElement; - Iterator _currentHook; - int _hookIndex; - List _hooks; + _Entry _currentHookState; + final LinkedList<_Entry> _hooks = LinkedList(); LinkedList<_Entry> _shouldRebuildQueue; LinkedList<_Entry> _needDispose; bool _isOptionalRebuild = false; Widget _buildCache; - bool _didFinishBuildOnce = false; - bool _debugDidReassemble; - bool _debugShouldDispose; - bool _debugIsInitHook; + bool _debugIsInitHook = false; + bool _debugDidReassemble = false; + + /// A read-only list of all hooks available. + /// + /// In release mode, returns `null`. + List get debugHooks { + if (!kDebugMode) { + return null; + } + return [ + for (final hook in _hooks) hook.value, + ]; + } @override void update(Widget newWidget) { @@ -314,6 +376,7 @@ mixin HookElement on ComponentElement { @override Widget build() { + // Check whether we can cancel the rebuild (caused by HookState.mayNeedRebuild). final mustRebuild = _isOptionalRebuild != true || _shouldRebuildQueue.any((cb) => cb.value()); @@ -324,22 +387,17 @@ mixin HookElement on ComponentElement { return _buildCache; } - _currentHook = _hooks?.iterator; - // first iterator always has null - _currentHook?.moveNext(); - _hookIndex = 0; - assert(() { - _debugShouldDispose = false; + if (kDebugMode) { _debugIsInitHook = false; - _debugDidReassemble ??= false; - return true; - }(), ''); - HookElement._currentContext = this; + } + _currentHookState = _hooks.isEmpty ? null : _hooks.first; + HookElement._currentHookElement = this; try { _buildCache = super.build(); } finally { - HookElement._currentContext = null; - if (_needDispose != null) { + _unmountAllRemainingHooks(); + HookElement._currentHookElement = null; + if (_needDispose != null && _needDispose.isNotEmpty) { for (var toDispose = _needDispose.last; toDispose != null; toDispose = toDispose.previous) { @@ -349,40 +407,42 @@ mixin HookElement on ComponentElement { } } - // dispose removed items - assert(() { - if (!debugHotReloadHooksEnabled) { - return true; - } - if (_debugDidReassemble && _hooks != null) { - while (_hookIndex < _hooks.length) { - _hooks.removeAt(_hookIndex).dispose(); - } - } - return true; - }(), ''); - assert(_hookIndex == (_hooks?.length ?? 0), ''' -Build for $widget finished with less hooks used than a previous build. -Used $_hookIndex hooks while a previous build had ${_hooks.length}. -This may happen if the call to `Hook.use` is made under some condition. + return _buildCache; + } + R _use(Hook hook) { + /// At the end of the hooks list + if (_currentHookState == null) { + _appendHook(hook); + } else if (hook.runtimeType != _currentHookState.value.hook.runtimeType) { + final previousHookType = _currentHookState.value.hook.runtimeType; + _unmountAllRemainingHooks(); + if (kDebugMode && _debugDidReassemble) { + _appendHook(hook); + } else { + throw StateError(''' +Type mismatch between hooks: +- previous hook: $previousHookType +- new hook: ${hook.runtimeType} '''); - assert(() { - if (!debugHotReloadHooksEnabled) { - return true; } - _debugDidReassemble = false; - return true; - }(), ''); - _didFinishBuildOnce = true; - return _buildCache; - } + } else if (hook != _currentHookState.value.hook) { + final previousHook = _currentHookState.value.hook; + if (Hook.shouldPreserveState(previousHook, hook)) { + _currentHookState.value + .._hook = hook + ..didUpdateHook(previousHook); + } else { + _needDispose ??= LinkedList(); + _needDispose.add(_Entry(_currentHookState.value)); + _currentHookState.value = _createHookState(hook); + } + } - /// A read-only list of all hooks available. - /// - /// These should not be used directly and are exposed - @visibleForTesting - List get debugHooks => List.unmodifiable(_hooks); + final result = _currentHookState.value.build(this) as R; + _currentHookState = _currentHookState.next; + return result; + } @override T dependOnInheritedWidgetOfExactType({ @@ -390,41 +450,19 @@ This may happen if the call to `Hook.use` is made under some condition. }) { assert( !_debugIsInitHook, - 'Cannot listen to inherited widgets inside HookState.initState. Use HookState.build instead', + 'Cannot listen to inherited widgets inside HookState.initState.' + ' Use HookState.build instead', ); return super.dependOnInheritedWidgetOfExactType(aspect: aspect); } - @override - Element updateChild(Element child, Widget newWidget, dynamic newSlot) { - if (_hooks != null) { - for (final hook in _hooks.reversed) { - try { - hook.didBuild(); - } catch (exception, stack) { - FlutterError.reportError( - FlutterErrorDetails( - exception: exception, - stack: stack, - library: 'hooks library', - context: DiagnosticsNode.message( - 'while calling `didBuild` on ${hook.runtimeType}', - ), - ), - ); - } - } - } - return super.updateChild(child, newWidget, newSlot); - } - @override void unmount() { super.unmount(); - if (_hooks != null) { - for (final hook in _hooks.reversed) { + if (_hooks != null && _hooks.isNotEmpty) { + for (var hook = _hooks.last; hook != null; hook = hook.previous) { try { - hook.dispose(); + hook.value.dispose(); } catch (exception, stack) { FlutterError.reportError( FlutterErrorDetails( @@ -446,7 +484,7 @@ This may happen if the call to `Hook.use` is made under some condition. if (_hooks != null) { for (final hook in _hooks) { try { - hook.deactivate(); + hook.value.deactivate(); } catch (exception, stack) { FlutterError.reportError( FlutterErrorDetails( @@ -467,139 +505,12 @@ This may happen if the call to `Hook.use` is made under some condition. @override void reassemble() { super.reassemble(); - assert(() { - _debugDidReassemble = true; - if (_hooks != null) { - for (final hook in _hooks) { - hook.reassemble(); - } - } - return true; - }(), ''); - } - - R _use(Hook hook) { - HookState> hookState; - // first build - if (_currentHook == null) { - assert( - _debugDidReassemble || !_didFinishBuildOnce, - 'No previous hook found at $_hookIndex, is a hook wrapped in a `if`?', - ); - hookState = _createHookState(hook); - _hooks ??= []; - _hooks.add(hookState); - } else { - // recreate states on hot-reload of the order changed - assert(() { - if (!debugHotReloadHooksEnabled) { - return true; - } - if (!_debugDidReassemble) { - return true; - } - if (!_debugShouldDispose && - _currentHook.current?.hook?.runtimeType == hook.runtimeType) { - return true; - } - _debugShouldDispose = true; - - // some previous hook has changed of type, so we dispose all the following states - // _currentHook.current can be null when reassemble is adding new hooks - if (_currentHook.current != null) { - _hooks.remove(_currentHook.current..dispose()); - // has to be done after the dispose call - hookState = _insertHookAt(_hookIndex, hook); - } else { - hookState = _pushHook(hook); - } - return true; - }(), ''); - if (!_didFinishBuildOnce && _currentHook.current == null) { - hookState = _pushHook(hook); - _currentHook.moveNext(); - } else { - assert( - _currentHook.current != null, - 'No previous hook found at $_hookIndex, is a hook wrapped in a `if`?', - ); - assert(_debugTypesAreRight(hook), ''); - - if (_currentHook.current.hook == hook) { - hookState = _currentHook.current as HookState>; - _currentHook.moveNext(); - } else if (Hook.shouldPreserveState(_currentHook.current.hook, hook)) { - hookState = _currentHook.current as HookState>; - _currentHook.moveNext(); - final previousHook = hookState._hook; - hookState - .._hook = hook - ..didUpdateHook(previousHook); - } else { - hookState = _replaceHookAt(_hookIndex, hook); - _resetsIterator(hookState); - _currentHook.moveNext(); - } + _debugDidReassemble = true; + if (_hooks != null) { + for (final hook in _hooks) { + hook.value.reassemble(); } } - _hookIndex++; - return hookState.build(this); - } - - HookState> _replaceHookAt(int index, Hook hook) { - _needDispose ??= LinkedList(); - _needDispose.add(_Entry(_hooks.removeAt(_hookIndex))); - // _hooks.removeAt(_hookIndex).dispose(); - final hookState = _createHookState(hook); - _hooks.insert(_hookIndex, hookState); - return hookState; - } - - HookState> _insertHookAt(int index, Hook hook) { - final hookState = _createHookState(hook); - _hooks.insert(index, hookState); - _resetsIterator(hookState); - return hookState; - } - - HookState> _pushHook(Hook hook) { - final hookState = _createHookState(hook); - _hooks.add(hookState); - _resetsIterator(hookState); - return hookState; - } - - bool _debugTypesAreRight(Hook hook) { - assert(_currentHook.current.hook.runtimeType == hook.runtimeType, - 'The previous and new hooks at index $_hookIndex do not match'); - return true; - } - - /// we put the iterator on added item - void _resetsIterator(HookState hookState) { - _currentHook = _hooks.iterator; - while (_currentHook.current != hookState) { - _currentHook.moveNext(); - } - } - - HookState> _createHookState(Hook hook) { - assert(() { - _debugIsInitHook = true; - return true; - }(), ''); - - final state = hook.createState() - .._element = this - .._hook = hook - ..initHook(); - - assert(() { - _debugIsInitHook = false; - return true; - }(), ''); - - return state; } } @@ -643,9 +554,11 @@ class _StatefulHookElement extends StatefulElement with HookElement { /// Obtain the [BuildContext] of the building [HookWidget]. BuildContext useContext() { - assert(HookElement._currentContext != null, - '`useContext` can only be called from the build method of HookWidget'); - return HookElement._currentContext; + assert( + HookElement._currentHookElement != null, + '`useContext` can only be called from the build method of HookWidget', + ); + return HookElement._currentHookElement; } /// A [HookWidget] that defer its `build` to a callback diff --git a/test/hook_builder_test.dart b/test/hook_builder_test.dart index b722993d..a4e502e4 100644 --- a/test/hook_builder_test.dart +++ b/test/hook_builder_test.dart @@ -1,29 +1,17 @@ import 'package:flutter/widgets.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:mockito/mockito.dart'; - -import 'mock.dart'; +import 'package:flutter_test/flutter_test.dart'; void main() { testWidgets('simple build', (tester) async { - final fn = Func1(); - when(fn.call(any)).thenAnswer((_) { - return Container(); - }); - - Widget createBuilder() => HookBuilder(builder: fn.call); - - final _builder = createBuilder(); - - await tester.pumpWidget(_builder); - - verify(fn.call(any)).called(1); - - await tester.pumpWidget(_builder); - verifyNever(fn.call(any)); + await tester.pumpWidget( + HookBuilder(builder: (context) { + final state = useState(42).value; + return Text('$state', textDirection: TextDirection.ltr); + }), + ); - await tester.pumpWidget(createBuilder()); - verify(fn.call(any)).called(1); + expect(find.text('42'), findsOneWidget); }); test('builder required', () { diff --git a/test/hook_widget_test.dart b/test/hook_widget_test.dart index 82d3cae8..b54b543e 100644 --- a/test/hook_widget_test.dart +++ b/test/hook_widget_test.dart @@ -20,39 +20,33 @@ class InheritedInitHookState extends HookState { } void main() { - final build = Func1(); - final dispose = Func0(); - final deactivate = Func0(); - final initHook = Func0(); - final didUpdateHook = Func1(); - final didBuild = Func0(); - final reassemble = Func0(); - final builder = Func1(); + final build = MockBuild(); + final dispose = MockDispose(); + final deactivate = MockDeactivate(); + final initHook = MockInitHook(); + final didUpdateHook = MockDidUpdateHook(); + final reassemble = MockReassemble(); HookTest createHook() { return HookTest( - build: build.call, - dispose: dispose.call, - didUpdateHook: didUpdateHook.call, - reassemble: reassemble.call, - initHook: initHook.call, - didBuild: didBuild, + build: build, + dispose: dispose, + didUpdateHook: didUpdateHook, + reassemble: reassemble, + initHook: initHook, deactivate: deactivate, ); } void verifyNoMoreHookInteration() { verifyNoMoreInteractions(build); - verifyNoMoreInteractions(didBuild); verifyNoMoreInteractions(dispose); verifyNoMoreInteractions(initHook); verifyNoMoreInteractions(didUpdateHook); } tearDown(() { - reset(builder); reset(build); - reset(didBuild); reset(dispose); reset(deactivate); reset(initHook); @@ -148,8 +142,8 @@ void main() { final _key1 = GlobalKey(); final _key2 = GlobalKey(); final state = ValueNotifier(false); - final deactivate1 = Func0(); - final deactivate2 = Func0(); + final deactivate1 = MockDeactivate(); + final deactivate2 = MockDeactivate(); await tester.pumpWidget( Directionality( @@ -190,8 +184,8 @@ void main() { await tester.pump(); verifyInOrder([ - deactivate1.call(), - deactivate2.call(), + deactivate1(), + deactivate2(), ]); await tester.pump(); @@ -202,17 +196,17 @@ void main() { testWidgets('should call other deactivates even if one fails', (tester) async { - final onError = Func1(); + final onError = MockOnError(); final oldOnError = FlutterError.onError; FlutterError.onError = onError; final errorBuilder = ErrorWidget.builder; - ErrorWidget.builder = Func1(); + ErrorWidget.builder = MockErrorBuilder(); when(ErrorWidget.builder(any)).thenReturn(Container()); - final deactivate = Func0(); - when(deactivate.call()).thenThrow(42); - final deactivate2 = Func0(); + final deactivate = MockDeactivate(); + when(deactivate()).thenThrow(42); + final deactivate2 = MockDeactivate(); final _key = GlobalKey(); @@ -238,14 +232,14 @@ void main() { deactivate2(), ]); - verify(onError.call(any)).called(1); + verify(onError(any)).called(1); verifyNoMoreInteractions(deactivate); verifyNoMoreInteractions(deactivate2); } finally { // reset the exception because after the test // flutter tries to deactivate the widget and it causes // and exception - when(deactivate.call()).thenAnswer((_) {}); + when(deactivate()).thenAnswer((_) {}); FlutterError.onError = oldOnError; ErrorWidget.builder = errorBuilder; } @@ -253,10 +247,12 @@ void main() { testWidgets('should not allow using inheritedwidgets inside initHook', (tester) async { - await tester.pumpWidget(HookBuilder(builder: (_) { - Hook.use(InheritedInitHook()); - return Container(); - })); + await tester.pumpWidget( + HookBuilder(builder: (_) { + Hook.use(InheritedInitHook()); + return Container(); + }), + ); expect(tester.takeException(), isAssertionError); }); @@ -269,21 +265,26 @@ void main() { return null; }); - await tester.pumpWidget(HookBuilder(builder: (_) { - Hook.use(HookTest(build: build)); - return Container(); - })); + await tester.pumpWidget( + HookBuilder(builder: (_) { + Hook.use(HookTest(build: build)); + return Container(); + }), + ); }); testWidgets("release mode don't crash", (tester) async { ValueNotifier notifier; debugHotReloadHooksEnabled = false; addTearDown(() => debugHotReloadHooksEnabled = true); - await tester.pumpWidget(HookBuilder(builder: (_) { - notifier = useState(0); + await tester.pumpWidget( + HookBuilder(builder: (_) { + notifier = useState(0); - return Text(notifier.value.toString(), textDirection: TextDirection.ltr); - })); + return Text(notifier.value.toString(), + textDirection: TextDirection.ltr); + }), + ); expect(find.text('0'), findsOneWidget); @@ -294,253 +295,271 @@ void main() { }); testWidgets('HookElement exposes an immutable list of hooks', (tester) async { - await tester.pumpWidget(HookBuilder(builder: (_) { - Hook.use(HookTest()); - Hook.use(HookTest()); - return Container(); - })); + await tester.pumpWidget( + HookBuilder(builder: (_) { + Hook.use(HookTest()); + Hook.use(HookTest()); + return Container(); + }), + ); final element = tester.element(find.byType(HookBuilder)) as HookElement; - expect(element.debugHooks.length, 2); - expect(element.debugHooks.first, isInstanceOf>()); - expect(element.debugHooks.last, isInstanceOf>()); - expect(() => element.debugHooks[0] = null, throwsUnsupportedError); - expect(() => element.debugHooks.add(null), throwsUnsupportedError); + expect(element.debugHooks, [ + isA>(), + isA>(), + ]); }); testWidgets( 'until one build finishes without crashing, it is possible to add hooks', (tester) async { - await tester.pumpWidget(HookBuilder(builder: (_) { - throw 0; - })); + await tester.pumpWidget( + HookBuilder(builder: (_) { + throw 0; + }), + ); expect(tester.takeException(), 0); - await tester.pumpWidget(HookBuilder(builder: (_) { - Hook.use(HookTest()); - throw 1; - })); + await tester.pumpWidget( + HookBuilder(builder: (_) { + Hook.use(HookTest()); + throw 1; + }), + ); expect(tester.takeException(), 1); - await tester.pumpWidget(HookBuilder(builder: (_) { - Hook.use(HookTest()); - Hook.use(HookTest()); - throw 2; - })); + await tester.pumpWidget( + HookBuilder(builder: (_) { + Hook.use(HookTest()); + Hook.use(HookTest()); + throw 2; + }), + ); expect(tester.takeException(), 2); - await tester.pumpWidget(HookBuilder(builder: (_) { - Hook.use(HookTest()); - Hook.use(HookTest()); - Hook.use(HookTest()); - return Container(); - })); + await tester.pumpWidget( + HookBuilder(builder: (_) { + Hook.use(HookTest()); + Hook.use(HookTest()); + Hook.use(HookTest()); + return Container(); + }), + ); }); testWidgets( 'until one build finishes without crashing, it is possible to add hooks #2', (tester) async { - await tester.pumpWidget(HookBuilder(builder: (_) { - throw 0; - })); + await tester.pumpWidget( + HookBuilder(builder: (_) { + throw 0; + }), + ); expect(tester.takeException(), 0); - await tester.pumpWidget(HookBuilder(builder: (_) { - Hook.use(HookTest()); - throw 1; - })); + await tester.pumpWidget( + HookBuilder(builder: (_) { + Hook.use(HookTest()); + throw 1; + }), + ); expect(tester.takeException(), 1); - await tester.pumpWidget(HookBuilder(builder: (_) { - Hook.use(HookTest()); - Hook.use(HookTest()); - Hook.use(HookTest()); - throw 2; - })); + await tester.pumpWidget( + HookBuilder(builder: (_) { + Hook.use(HookTest()); + Hook.use(HookTest()); + Hook.use(HookTest()); + throw 2; + }), + ); expect(tester.takeException(), 2); }); - testWidgets( - 'After a build suceeded, expections do not allow adding more hooks', - (tester) async { - await tester.pumpWidget(HookBuilder(builder: (_) { - return Container(); - })); - - await tester.pumpWidget(HookBuilder(builder: (_) { - throw 1; - })); - expect(tester.takeException(), 1); - - await tester.pumpWidget(HookBuilder(builder: (_) { - Hook.use(HookTest()); - return Container(); - })); - expect(tester.takeException(), isAssertionError); - }); - testWidgets( "After hot-reload that throws it's still possible to add hooks until one build suceed", (tester) async { - await tester.pumpWidget(HookBuilder(builder: (_) { - return Container(); - })); + await tester.pumpWidget( + HookBuilder(builder: (_) { + return Container(); + }), + ); hotReload(tester); - await tester.pumpWidget(HookBuilder(builder: (_) { - throw 0; - })); + await tester.pumpWidget( + HookBuilder(builder: (_) { + throw 0; + }), + ); expect(tester.takeException(), 0); - await tester.pumpWidget(HookBuilder(builder: (_) { - Hook.use(HookTest()); - return Container(); - })); + await tester.pumpWidget( + HookBuilder(builder: (_) { + Hook.use(HookTest()); + return Container(); + }), + ); }); testWidgets( 'After hot-reload that throws, hooks are correctly disposed when build suceeeds with less hooks', (tester) async { - await tester.pumpWidget(HookBuilder(builder: (_) { - Hook.use(createHook()); - return Container(); - })); + await tester.pumpWidget( + HookBuilder(builder: (_) { + Hook.use(createHook()); + return Container(); + }), + ); hotReload(tester); - await tester.pumpWidget(HookBuilder(builder: (_) { - throw 0; - })); - expect(tester.takeException(), 0); - verifyNever(dispose()); + await tester.pumpWidget( + HookBuilder(builder: (_) { + throw 0; + }), + ); - await tester.pumpWidget(HookBuilder(builder: (_) { - return Container(); - })); + expect(tester.takeException(), 0); verify(dispose()).called(1); + verifyNoMoreInteractions(dispose); + + await tester.pumpWidget( + HookBuilder(builder: (_) { + return Container(); + }), + ); + + verifyNoMoreInteractions(dispose); }); testWidgets('hooks can be disposed independently with keys', (tester) async { - List keys; - List keys2; + final dispose2 = MockDispose(); - final dispose2 = Func0(); - when(builder.call(any)).thenAnswer((invocation) { - Hook.use(HookTest(dispose: dispose.call, keys: keys)); - Hook.use(HookTest(dispose: dispose2.call, keys: keys2)); - return Container(); - }); - await tester.pumpWidget(HookBuilder(builder: builder.call)); + await tester.pumpWidget( + HookBuilder(builder: (context) { + Hook.use(HookTest(dispose: dispose)); + Hook.use(HookTest(dispose: dispose2)); + return Container(); + }), + ); verifyZeroInteractions(dispose); verifyZeroInteractions(dispose2); - keys = []; - await tester.pumpWidget(HookBuilder(builder: builder.call)); + await tester.pumpWidget( + HookBuilder(builder: (context) { + Hook.use(HookTest(dispose: dispose, keys: const [])); + Hook.use(HookTest(dispose: dispose2)); + return Container(); + }), + ); - verify(dispose.call()).called(1); + verify(dispose()).called(1); verifyZeroInteractions(dispose2); - keys2 = []; - await tester.pumpWidget(HookBuilder(builder: builder.call)); + await tester.pumpWidget( + HookBuilder(builder: (context) { + Hook.use(HookTest(dispose: dispose, keys: const [])); + Hook.use(HookTest(dispose: dispose2, keys: const [])); + return Container(); + }), + ); - verify(dispose2.call()).called(1); + verify(dispose2()).called(1); verifyNoMoreInteractions(dispose); }); testWidgets('keys recreate hookstate', (tester) async { List keys; - final createState = Func0>(); - when(createState.call()).thenReturn(HookStateTest()); - - when(builder.call(any)).thenAnswer((invocation) { - Hook.use(HookTest( - build: build.call, - dispose: dispose.call, - didUpdateHook: didUpdateHook.call, - initHook: initHook.call, - keys: keys, - didBuild: didBuild, - createStateFn: createState.call, - )); - return Container(); - }); - await tester.pumpWidget(HookBuilder(builder: builder.call)); + final createState = MockCreateState>(); + when(createState()).thenReturn(HookStateTest()); + + Widget $build() { + return HookBuilder(builder: (context) { + Hook.use( + HookTest( + build: build, + dispose: dispose, + didUpdateHook: didUpdateHook, + initHook: initHook, + keys: keys, + createStateFn: createState, + ), + ); + return Container(); + }); + } - final context = find.byType(HookBuilder).evaluate().first; + await tester.pumpWidget($build()); + + final context = tester.element(find.byType(HookBuilder)); verifyInOrder([ - createState.call(), - initHook.call(), - build.call(context), - didBuild.call(), + createState(), + initHook(), + build(context), ]); verifyNoMoreHookInteration(); - await tester.pumpWidget(HookBuilder(builder: builder.call)); + await tester.pumpWidget($build()); verifyInOrder([ - didUpdateHook.call(any), - build.call(context), - didBuild.call(), + didUpdateHook(any), + build(context), ]); verifyNoMoreHookInteration(); // from null to array keys = []; - await tester.pumpWidget(HookBuilder(builder: builder.call)); + await tester.pumpWidget($build()); verifyInOrder([ - createState.call(), - initHook.call(), - build.call(context), - dispose.call(), - didBuild.call(), + createState(), + initHook(), + build(context), + dispose(), ]); verifyNoMoreHookInteration(); // array immutable keys.add(42); - await tester.pumpWidget(HookBuilder(builder: builder.call)); + await tester.pumpWidget($build()); verifyInOrder([ - didUpdateHook.call(any), - build.call(context), - didBuild.call(), + didUpdateHook(any), + build(context), ]); verifyNoMoreHookInteration(); // new array but content equal keys = [42]; - await tester.pumpWidget(HookBuilder(builder: builder.call)); + await tester.pumpWidget($build()); verifyInOrder([ - didUpdateHook.call(any), - build.call(context), - didBuild.call(), + didUpdateHook(any), + build(context), ]); verifyNoMoreHookInteration(); // new array new content keys = [44]; - await tester.pumpWidget(HookBuilder(builder: builder.call)); + await tester.pumpWidget($build()); verifyInOrder([ - createState.call(), - initHook.call(), - build.call(context), - dispose.call(), - didBuild.call() + createState(), + initHook(), + build(context), + dispose(), ]); verifyNoMoreHookInteration(); }); testWidgets('hook & setState', (tester) async { - final setState = Func0(); + final setState = MockSetState(); final hook = MyHook(); HookElement hookContext; MyHookState state; @@ -557,132 +576,50 @@ void main() { expect(state.context, hookContext); expect(hookContext.dirty, false); - state.setState(setState.call); - verify(setState.call()).called(1); + state.setState(setState); + verify(setState()).called(1); expect(hookContext.dirty, true); }); - testWidgets( - 'didBuild when build crash called after FlutterError.onError report', - (tester) async { - final onError = FlutterError.onError; - FlutterError.onError = Func1(); - final errorBuilder = ErrorWidget.builder; - ErrorWidget.builder = Func1(); - when(ErrorWidget.builder(any)).thenReturn(Container()); - try { - when(build.call(any)).thenThrow(42); - when(builder.call(any)).thenAnswer((invocation) { - Hook.use(createHook()); - return Container(); - }); - - await tester.pumpWidget(HookBuilder( - builder: builder.call, - )); - tester.takeException(); - - verifyInOrder([ - build.call(any), - FlutterError.onError(any), - ErrorWidget.builder(any), - didBuild(), - ]); - } finally { - FlutterError.onError = onError; - ErrorWidget.builder = errorBuilder; - } - }); - - testWidgets('didBuild called even if build crashed', (tester) async { - when(build.call(any)).thenThrow(42); - when(builder.call(any)).thenAnswer((invocation) { - Hook.use(createHook()); - return Container(); - }); - - await tester.pumpWidget(HookBuilder( - builder: builder.call, - )); - expect(tester.takeException(), 42); - - verify(didBuild.call()).called(1); - }); - testWidgets('all didBuild called even if one crashes', (tester) async { - final didBuild2 = Func0(); - - when(didBuild.call()).thenThrow(42); - when(builder.call(any)).thenAnswer((invocation) { - Hook.use(createHook()); - Hook.use(HookTest(didBuild: didBuild2)); - return Container(); - }); - - await expectPump( - () => tester.pumpWidget(HookBuilder( - builder: builder.call, - )), - throwsA(42), - ); - - verifyInOrder([ - didBuild2.call(), - didBuild.call(), - ]); - }); - - testWidgets('calls didBuild before building children', (tester) async { - final buildChild = Func1(); - when(buildChild.call(any)).thenReturn(Container()); - - await tester.pumpWidget(HookBuilder( - builder: (context) { - Hook.use(createHook()); - return Builder(builder: buildChild); - }, - )); - - verifyInOrder([ - didBuild(), - buildChild.call(any), - ]); - }); - testWidgets('life-cycles in order', (tester) async { int result; HookTest hook; - when(build.call(any)).thenReturn(42); - when(builder.call(any)).thenAnswer((invocation) { - hook = createHook(); - result = Hook.use(hook); - return Container(); - }); + when(build(any)).thenReturn(42); await tester.pumpWidget(HookBuilder( - builder: builder.call, + builder: (context) { + hook = createHook(); + result = Hook.use(hook); + return Container(); + }, )); final context = tester.firstElement(find.byType(HookBuilder)); expect(result, 42); verifyInOrder([ - initHook.call(), - build.call(context), - didBuild.call(), + initHook(), + build(context), ]); verifyNoMoreHookInteration(); - when(build.call(context)).thenReturn(24); + when(build(context)).thenReturn(24); var previousHook = hook; await tester.pumpWidget(HookBuilder( - builder: builder.call, + builder: (context) { + hook = createHook(); + result = Hook.use(hook); + return Container(); + }, )); expect(result, 24); - verifyInOrder( - [didUpdateHook.call(previousHook), build.call(any), didBuild.call()]); + verifyInOrder([ + didUpdateHook(previousHook), + build(any), + ]); verifyNoMoreHookInteration(); previousHook = hook; @@ -692,30 +629,31 @@ void main() { await tester.pumpWidget(const SizedBox()); - verify(dispose.call()).called(1); + verify(dispose()).called(1); verifyNoMoreHookInteration(); }); testWidgets('dispose all called even on failed', (tester) async { - final dispose2 = Func0(); + final dispose2 = MockDispose(); - when(build.call(any)).thenReturn(42); - when(builder.call(any)).thenAnswer((invocation) { - Hook.use(createHook()); - Hook.use(HookTest(dispose: dispose2)); - return Container(); - }); + when(build(any)).thenReturn(42); - await tester.pumpWidget(HookBuilder(builder: builder.call)); + await tester.pumpWidget( + HookBuilder(builder: (context) { + Hook.use(createHook()); + Hook.use(HookTest(dispose: dispose2)); + return Container(); + }), + ); - when(dispose.call()).thenThrow(24); + when(dispose()).thenThrow(24); await tester.pumpWidget(const SizedBox()); expect(tester.takeException(), 24); verifyInOrder([ - dispose2.call(), - dispose.call(), + dispose2(), + dispose(), ]); }); @@ -723,88 +661,102 @@ void main() { (tester) async { final hook = createHook(); - when(builder.call(any)).thenAnswer((invocation) { - Hook.use(hook); - return Container(); - }); - - await tester.pumpWidget(HookBuilder(builder: builder.call)); + await tester.pumpWidget( + HookBuilder(builder: (context) { + Hook.use(hook); + return Container(); + }), + ); verifyInOrder([ - initHook.call(), - build.call(any), + initHook(), + build(any), ]); verifyZeroInteractions(didUpdateHook); verifyZeroInteractions(dispose); - await tester.pumpWidget(HookBuilder(builder: builder.call)); + await tester.pumpWidget( + HookBuilder(builder: (context) { + Hook.use(hook); + return Container(); + }), + ); verifyInOrder([ - build.call(any), + build(any), ]); - verifyNever(didUpdateHook.call(any)); - verifyNever(initHook.call()); - verifyNever(dispose.call()); + verifyNever(didUpdateHook(any)); + verifyNever(initHook()); + verifyNever(dispose()); }); testWidgets('rebuild with different hooks crash', (tester) async { - when(builder.call(any)).thenAnswer((invocation) { - Hook.use(HookTest()); - return Container(); - }); - - await tester.pumpWidget(HookBuilder(builder: builder.call)); - - when(builder.call(any)).thenAnswer((invocation) { - Hook.use(HookTest()); - return Container(); - }); + await tester.pumpWidget( + HookBuilder(builder: (context) { + Hook.use(HookTest()); + return Container(); + }), + ); - await expectPump( - () => tester.pumpWidget(HookBuilder(builder: builder.call)), - throwsAssertionError, + await tester.pumpWidget( + HookBuilder(builder: (context) { + Hook.use(HookTest()); + return Container(); + }), ); + + expect(tester.takeException(), isStateError); }); - testWidgets('rebuild added hooks crash', (tester) async { - when(builder.call(any)).thenAnswer((invocation) { - Hook.use(HookTest()); - return Container(); - }); + testWidgets('rebuilds can add new hooks', (tester) async { + await tester.pumpWidget( + HookBuilder(builder: (context) { + final a = useState(false).value; + return Text('$a', textDirection: TextDirection.ltr); + }), + ); - await tester.pumpWidget(HookBuilder(builder: builder.call)); + expect(find.text('false'), findsOneWidget); - when(builder.call(any)).thenAnswer((invocation) { - Hook.use(HookTest()); - Hook.use(HookTest()); - return Container(); - }); + await tester.pumpWidget( + HookBuilder(builder: (context) { + final a = useState(true).value; + final b = useState(42).value; - await tester.pumpWidget(HookBuilder(builder: builder.call)); - expect(tester.takeException(), isAssertionError); + return Text('$a $b', textDirection: TextDirection.ltr); + }), + ); + + expect(find.text('false 42'), findsOneWidget); }); - testWidgets('rebuild removed hooks crash', (tester) async { - when(builder.call(any)).thenAnswer((invocation) { - Hook.use(HookTest()); - return Container(); - }); + testWidgets('rebuild can remove hooks', (tester) async { + await tester.pumpWidget( + HookBuilder(builder: (context) { + final a = useState(false).value; + final b = useState(42).value; - await tester.pumpWidget(HookBuilder(builder: builder.call)); + return Text('$a $b', textDirection: TextDirection.ltr); + }), + ); - when(builder.call(any)).thenAnswer((invocation) { - return Container(); - }); + expect(find.text('false 42'), findsOneWidget); - await expectPump( - () => tester.pumpWidget(HookBuilder(builder: builder.call)), - throwsAssertionError, + await tester.pumpWidget( + HookBuilder(builder: (context) { + final a = useState(true).value; + return Text('$a', textDirection: TextDirection.ltr); + }), ); + + expect(find.text('false'), findsOneWidget); }); testWidgets('use call outside build crash', (tester) async { - when(builder.call(any)).thenAnswer((invocation) => Container()); - - await tester.pumpWidget(HookBuilder(builder: builder.call)); + await tester.pumpWidget( + HookBuilder(builder: (context) { + return Container(); + }), + ); expect(() => Hook.use(HookTest()), throwsAssertionError); }); @@ -813,46 +765,53 @@ void main() { int result; HookTest previousHook; - when(build.call(any)).thenReturn(42); - when(builder.call(any)).thenAnswer((invocation) { - previousHook = createHook(); - result = Hook.use(previousHook); - return Container(); - }); + when(build(any)).thenReturn(42); - await tester.pumpWidget(HookBuilder(builder: builder.call)); + await tester.pumpWidget( + HookBuilder(builder: (context) { + previousHook = createHook(); + result = Hook.use(previousHook); + return Container(); + }), + ); expect(result, 42); verifyInOrder([ - initHook.call(), - build.call(any), + initHook(), + build(any), ]); verifyZeroInteractions(didUpdateHook); verifyZeroInteractions(dispose); - when(build.call(any)).thenReturn(24); + when(build(any)).thenReturn(24); hotReload(tester); await tester.pump(); expect(result, 24); verifyInOrder([ - didUpdateHook.call(any), - build.call(any), + didUpdateHook(any), + build(any), ]); - verifyNever(initHook.call()); - verifyNever(dispose.call()); + verifyNever(initHook()); + verifyNever(dispose()); }); testWidgets('hot-reload calls reassemble', (tester) async { - final reassemble2 = Func0(); - final didUpdateHook2 = Func1>(); - await tester.pumpWidget(HookBuilder(builder: (context) { - Hook.use(createHook()); - Hook.use(HookTest( - reassemble: reassemble2, didUpdateHook: didUpdateHook2)); - return Container(); - })); + final reassemble2 = MockReassemble(); + final didUpdateHook2 = MockDidUpdateHook(); + await tester.pumpWidget( + HookBuilder(builder: (context) { + Hook.use(createHook()); + Hook.use( + HookTest( + reassemble: reassemble2, + didUpdateHook: didUpdateHook2, + ), + ); + return Container(); + }), + ); verifyNoMoreInteractions(reassemble); @@ -860,28 +819,32 @@ void main() { await tester.pump(); verifyInOrder([ - reassemble.call(), - reassemble2.call(), - didUpdateHook.call(any), - didUpdateHook2.call(any), + reassemble(), + reassemble2(), + didUpdateHook(any), + didUpdateHook2(any), ]); verifyNoMoreInteractions(reassemble); }); testWidgets("hot-reload don't reassemble newly added hooks", (tester) async { - await tester.pumpWidget(HookBuilder(builder: (context) { - Hook.use(HookTest()); - return Container(); - })); + await tester.pumpWidget( + HookBuilder(builder: (context) { + Hook.use(HookTest()); + return Container(); + }), + ); verifyNoMoreInteractions(reassemble); hotReload(tester); - await tester.pumpWidget(HookBuilder(builder: (context) { - Hook.use(HookTest()); - Hook.use(createHook()); - return Container(); - })); + await tester.pumpWidget( + HookBuilder(builder: (context) { + Hook.use(HookTest()); + Hook.use(createHook()); + return Container(); + }), + ); verifyNoMoreInteractions(didUpdateHook); verifyNoMoreInteractions(reassemble); @@ -891,46 +854,49 @@ void main() { (tester) async { HookTest hook1; - final dispose2 = Func0(); - final initHook2 = Func0(); - final didUpdateHook2 = Func1(); - final build2 = Func1(); - - when(builder.call(any)).thenAnswer((invocation) { - Hook.use(hook1 = createHook()); - return Container(); - }); + final dispose2 = MockDispose(); + final initHook2 = MockInitHook(); + final didUpdateHook2 = MockDidUpdateHook(); + final build2 = MockBuild(); - await tester.pumpWidget(HookBuilder(builder: builder.call)); + await tester.pumpWidget( + HookBuilder(builder: (context) { + Hook.use(hook1 = createHook()); + return Container(); + }), + ); - final context = find.byType(HookBuilder).evaluate().first; + final context = tester.element(find.byType(HookBuilder)); verifyInOrder([ - initHook.call(), - build.call(context), + initHook(), + build(context), ]); verifyZeroInteractions(dispose); verifyZeroInteractions(didUpdateHook); - when(builder.call(any)).thenAnswer((invocation) { - Hook.use(createHook()); - Hook.use(HookTest( - initHook: initHook2, - build: build2, - didUpdateHook: didUpdateHook2, - dispose: dispose2, - )); - return Container(); - }); - hotReload(tester); - await tester.pump(); + + await tester.pumpWidget( + HookBuilder(builder: (context) { + Hook.use(createHook()); + Hook.use( + HookTest( + initHook: initHook2, + build: build2, + didUpdateHook: didUpdateHook2, + dispose: dispose2, + ), + ); + return Container(); + }), + ); verifyInOrder([ - didUpdateHook.call(hook1), - build.call(context), - initHook2.call(), - build2.call(context), + didUpdateHook(hook1), + build(context), + initHook2(), + build2(context), ]); verifyNoMoreInteractions(initHook); verifyZeroInteractions(dispose); @@ -940,47 +906,48 @@ void main() { testWidgets('hot-reload can add hooks in the middle of the list', (tester) async { - final dispose2 = Func0(); - final initHook2 = Func0(); - final didUpdateHook2 = Func1(); - final build2 = Func1(); - - when(builder.call(any)).thenAnswer((invocation) { - Hook.use(createHook()); - return Container(); - }); + final dispose2 = MockDispose(); + final initHook2 = MockInitHook(); + final didUpdateHook2 = MockDidUpdateHook(); + final build2 = MockBuild(); - await tester.pumpWidget(HookBuilder(builder: builder.call)); + await tester.pumpWidget( + HookBuilder(builder: (context) { + Hook.use(createHook()); + return Container(); + }), + ); - final context = find.byType(HookBuilder).evaluate().first; + final context = tester.element(find.byType(HookBuilder)); verifyInOrder([ - initHook.call(), - build.call(context), + initHook(), + build(context), ]); verifyZeroInteractions(dispose); verifyZeroInteractions(didUpdateHook); - when(builder.call(any)).thenAnswer((invocation) { - Hook.use(HookTest( - initHook: initHook2, - build: build2, - didUpdateHook: didUpdateHook2, - dispose: dispose2, - )); - Hook.use(createHook()); - return Container(); - }); - hotReload(tester); - await tester.pump(); + + await tester.pumpWidget( + HookBuilder(builder: (context) { + Hook.use(HookTest( + initHook: initHook2, + build: build2, + didUpdateHook: didUpdateHook2, + dispose: dispose2, + )); + Hook.use(createHook()); + return Container(); + }), + ); verifyInOrder([ - dispose.call(), - initHook2.call(), - build2.call(context), - initHook.call(), - build.call(context), + initHook2(), + build2(context), + initHook(), + build(context), + dispose(), ]); verifyNoMoreInteractions(didUpdateHook); verifyNoMoreInteractions(dispose); @@ -988,30 +955,32 @@ void main() { verifyZeroInteractions(didUpdateHook2); }); testWidgets('hot-reload can remove hooks', (tester) async { - final dispose2 = Func0(); - final initHook2 = Func0(); - final didUpdateHook2 = Func1(); - final build2 = Func1(); - - when(builder.call(any)).thenAnswer((invocation) { - Hook.use(createHook()); - Hook.use(HookTest( - initHook: initHook2, - build: build2, - didUpdateHook: didUpdateHook2, - dispose: dispose2, - )); - return Container(); - }); + final dispose2 = MockDispose(); + final initHook2 = MockInitHook(); + final didUpdateHook2 = MockDidUpdateHook(); + final build2 = MockBuild(); - await tester.pumpWidget(HookBuilder(builder: builder.call)); - final context = find.byType(HookBuilder).evaluate().first; + await tester.pumpWidget( + HookBuilder(builder: (context) { + Hook.use(createHook()); + Hook.use( + HookTest( + initHook: initHook2, + build: build2, + didUpdateHook: didUpdateHook2, + dispose: dispose2, + ), + ); + return Container(); + }), + ); + final context = tester.element(find.byType(HookBuilder)); verifyInOrder([ - initHook.call(), - build.call(context), - initHook2.call(), - build2.call(context), + initHook(), + build(context), + initHook2(), + build2(context), ]); verifyZeroInteractions(dispose); @@ -1019,16 +988,17 @@ void main() { verifyZeroInteractions(dispose2); verifyZeroInteractions(didUpdateHook2); - when(builder.call(any)).thenAnswer((invocation) { - return Container(); - }); - hotReload(tester); - await tester.pumpWidget(HookBuilder(builder: builder.call)); + + await tester.pumpWidget( + HookBuilder(builder: (context) { + return Container(); + }), + ); verifyInOrder([ - dispose.call(), - dispose2.call(), + dispose2(), + dispose(), ]); verifyNoMoreInteractions(initHook); @@ -1042,32 +1012,32 @@ void main() { testWidgets('hot-reload disposes hooks when type change', (tester) async { HookTest hook1; - final dispose2 = Func0(); - final initHook2 = Func0(); - final didUpdateHook2 = Func1(); - final build2 = Func1(); - - final dispose3 = Func0(); - final initHook3 = Func0(); - final didUpdateHook3 = Func1(); - final build3 = Func1(); - - final dispose4 = Func0(); - final initHook4 = Func0(); - final didUpdateHook4 = Func1(); - final build4 = Func1(); - - when(builder.call(any)).thenAnswer((invocation) { - Hook.use(hook1 = createHook()); - Hook.use(HookTest(dispose: dispose2)); - Hook.use(HookTest(dispose: dispose3)); - Hook.use(HookTest(dispose: dispose4)); - return Container(); - }); + final dispose2 = MockDispose(); + final initHook2 = MockInitHook(); + final didUpdateHook2 = MockDidUpdateHook(); + final build2 = MockBuild(); + + final dispose3 = MockDispose(); + final initHook3 = MockInitHook(); + final didUpdateHook3 = MockDidUpdateHook(); + final build3 = MockBuild(); + + final dispose4 = MockDispose(); + final initHook4 = MockInitHook(); + final didUpdateHook4 = MockDidUpdateHook(); + final build4 = MockBuild(); - await tester.pumpWidget(HookBuilder(builder: builder.call)); + await tester.pumpWidget( + HookBuilder(builder: (context) { + Hook.use(hook1 = createHook()); + Hook.use(HookTest(dispose: dispose2)); + Hook.use(HookTest(dispose: dispose3)); + Hook.use(HookTest(dispose: dispose4)); + return Container(); + }), + ); - final context = find.byType(HookBuilder).evaluate().first; + final context = tester.element(find.byType(HookBuilder)); // We don't care about datas of the first render clearInteractions(initHook); @@ -1090,42 +1060,49 @@ void main() { clearInteractions(dispose4); clearInteractions(build4); - when(builder.call(any)).thenAnswer((invocation) { - Hook.use(createHook()); - // changed type from HookTest - Hook.use(HookTest( - initHook: initHook2, - build: build2, - didUpdateHook: didUpdateHook2, - )); - Hook.use(HookTest( - initHook: initHook3, - build: build3, - didUpdateHook: didUpdateHook3, - )); - Hook.use(HookTest( - initHook: initHook4, - build: build4, - didUpdateHook: didUpdateHook4, - )); - return Container(); - }); - hotReload(tester); - await tester.pump(); + + await tester.pumpWidget( + HookBuilder(builder: (context) { + Hook.use(createHook()); + // changed type from HookTest + Hook.use( + HookTest( + initHook: initHook2, + build: build2, + didUpdateHook: didUpdateHook2, + ), + ); + Hook.use( + HookTest( + initHook: initHook3, + build: build3, + didUpdateHook: didUpdateHook3, + ), + ); + Hook.use( + HookTest( + initHook: initHook4, + build: build4, + didUpdateHook: didUpdateHook4, + ), + ); + return Container(); + }), + ); verifyInOrder([ - didUpdateHook.call(hook1), - build.call(context), - dispose2.call(), - initHook2.call(), - build2.call(context), - dispose3.call(), - initHook3.call(), - build3.call(context), - dispose4.call(), - initHook4.call(), - build4.call(context), + didUpdateHook(hook1), + build(context), + initHook2(), + build2(context), + initHook3(), + build3(context), + initHook4(), + build4(context), + dispose4(), + dispose3(), + dispose2(), ]); verifyZeroInteractions(initHook); verifyZeroInteractions(dispose); @@ -1137,32 +1114,32 @@ void main() { testWidgets('hot-reload disposes hooks when type change', (tester) async { HookTest hook1; - final dispose2 = Func0(); - final initHook2 = Func0(); - final didUpdateHook2 = Func1(); - final build2 = Func1(); - - final dispose3 = Func0(); - final initHook3 = Func0(); - final didUpdateHook3 = Func1(); - final build3 = Func1(); - - final dispose4 = Func0(); - final initHook4 = Func0(); - final didUpdateHook4 = Func1(); - final build4 = Func1(); - - when(builder.call(any)).thenAnswer((invocation) { - Hook.use(hook1 = createHook()); - Hook.use(HookTest(dispose: dispose2)); - Hook.use(HookTest(dispose: dispose3)); - Hook.use(HookTest(dispose: dispose4)); - return Container(); - }); + final dispose2 = MockDispose(); + final initHook2 = MockInitHook(); + final didUpdateHook2 = MockDidUpdateHook(); + final build2 = MockBuild(); + + final dispose3 = MockDispose(); + final initHook3 = MockInitHook(); + final didUpdateHook3 = MockDidUpdateHook(); + final build3 = MockBuild(); - await tester.pumpWidget(HookBuilder(builder: builder.call)); + final dispose4 = MockDispose(); + final initHook4 = MockInitHook(); + final didUpdateHook4 = MockDidUpdateHook(); + final build4 = MockBuild(); + + await tester.pumpWidget( + HookBuilder(builder: (context) { + Hook.use(hook1 = createHook()); + Hook.use(HookTest(dispose: dispose2)); + Hook.use(HookTest(dispose: dispose3)); + Hook.use(HookTest(dispose: dispose4)); + return Container(); + }), + ); - final context = find.byType(HookBuilder).evaluate().first; + final context = tester.element(find.byType(HookBuilder)); // We don't care about datas of the first render clearInteractions(initHook); @@ -1185,42 +1162,42 @@ void main() { clearInteractions(dispose4); clearInteractions(build4); - when(builder.call(any)).thenAnswer((invocation) { - Hook.use(createHook()); - // changed type from HookTest - Hook.use(HookTest( - initHook: initHook2, - build: build2, - didUpdateHook: didUpdateHook2, - )); - Hook.use(HookTest( - initHook: initHook3, - build: build3, - didUpdateHook: didUpdateHook3, - )); - Hook.use(HookTest( - initHook: initHook4, - build: build4, - didUpdateHook: didUpdateHook4, - )); - return Container(); - }); - hotReload(tester); - await tester.pump(); + await tester.pumpWidget( + HookBuilder(builder: (context) { + Hook.use(createHook()); + // changed type from HookTest + Hook.use(HookTest( + initHook: initHook2, + build: build2, + didUpdateHook: didUpdateHook2, + )); + Hook.use(HookTest( + initHook: initHook3, + build: build3, + didUpdateHook: didUpdateHook3, + )); + Hook.use(HookTest( + initHook: initHook4, + build: build4, + didUpdateHook: didUpdateHook4, + )); + return Container(); + }), + ); verifyInOrder([ - didUpdateHook.call(hook1), - build.call(context), - dispose2.call(), - initHook2.call(), - build2.call(context), - dispose3.call(), - initHook3.call(), - build3.call(context), - dispose4.call(), - initHook4.call(), - build4.call(context), + didUpdateHook(hook1), + build(context), + initHook2(), + build2(context), + initHook3(), + build3(context), + initHook4(), + build4(context), + dispose4(), + dispose3(), + dispose2(), ]); verifyZeroInteractions(initHook); verifyZeroInteractions(dispose); @@ -1230,21 +1207,17 @@ void main() { }); testWidgets('hot-reload without hooks do not crash', (tester) async { - when(builder.call(any)).thenAnswer((invocation) { - return Container(); - }); - - await tester.pumpWidget(HookBuilder(builder: builder.call)); + await tester.pumpWidget( + HookBuilder(builder: (c) { + return Container(); + }), + ); hotReload(tester); - await expectPump(() => tester.pump(), completes); + await tester.pump(); }); } -class MockDispose extends Mock { - void call(); -} - class MyHook extends Hook { @override MyHookState createState() => MyHookState(); diff --git a/test/memoized_test.dart b/test/memoized_test.dart index e2dc0281..8fd420a1 100644 --- a/test/memoized_test.dart +++ b/test/memoized_test.dart @@ -4,27 +4,28 @@ import 'package:flutter_hooks/flutter_hooks.dart'; import 'mock.dart'; void main() { - final builder = Func1(); - final parameterBuilder = Func0>(); - final valueBuilder = Func0(); + final valueBuilder = MockValueBuilder(); tearDown(() { - reset(builder); reset(valueBuilder); - reset(parameterBuilder); }); testWidgets('invalid parameters', (tester) async { - await tester.pumpWidget(HookBuilder(builder: (context) { - useMemoized(null); - return Container(); - })); + await tester.pumpWidget( + HookBuilder(builder: (context) { + useMemoized(null); + return Container(); + }), + ); + expect(tester.takeException(), isAssertionError); - await tester.pumpWidget(HookBuilder(builder: (context) { - useMemoized(() {}, null); - return Container(); - })); + await tester.pumpWidget( + HookBuilder(builder: (context) { + useMemoized(() {}, null); + return Container(); + }), + ); expect(tester.takeException(), isAssertionError); }); @@ -32,20 +33,25 @@ void main() { (tester) async { int result; - when(valueBuilder.call()).thenReturn(42); - - when(builder.call(any)).thenAnswer((invocation) { - result = useMemoized(valueBuilder.call); - return Container(); - }); + when(valueBuilder()).thenReturn(42); - await tester.pumpWidget(HookBuilder(builder: builder.call)); + await tester.pumpWidget( + HookBuilder(builder: (context) { + result = useMemoized(valueBuilder); + return Container(); + }), + ); - verify(valueBuilder.call()).called(1); + verify(valueBuilder()).called(1); verifyNoMoreInteractions(valueBuilder); expect(result, 42); - await tester.pumpWidget(HookBuilder(builder: builder.call)); + await tester.pumpWidget( + HookBuilder(builder: (context) { + result = useMemoized(valueBuilder); + return Container(); + }), + ); verifyNoMoreInteractions(valueBuilder); expect(result, 42); @@ -60,59 +66,81 @@ void main() { (tester) async { int result; - when(valueBuilder.call()).thenReturn(0); - when(parameterBuilder.call()).thenReturn([]); + when(valueBuilder()).thenReturn(0); - when(builder.call(any)).thenAnswer((invocation) { - result = useMemoized(valueBuilder.call, parameterBuilder.call()); - return Container(); - }); + await tester.pumpWidget( + HookBuilder(builder: (context) { + result = useMemoized(valueBuilder, []); + return Container(); + }), + ); - await tester.pumpWidget(HookBuilder(builder: builder.call)); - - verify(valueBuilder.call()).called(1); + verify(valueBuilder()).called(1); verifyNoMoreInteractions(valueBuilder); expect(result, 0); /* No change */ - await tester.pumpWidget(HookBuilder(builder: builder.call)); + await tester.pumpWidget( + HookBuilder(builder: (context) { + result = useMemoized(valueBuilder, []); + return Container(); + }), + ); verifyNoMoreInteractions(valueBuilder); expect(result, 0); /* Add parameter */ - when(parameterBuilder.call()).thenReturn(['foo']); - when(valueBuilder.call()).thenReturn(1); + when(valueBuilder()).thenReturn(1); - await tester.pumpWidget(HookBuilder(builder: builder.call)); + await tester.pumpWidget( + HookBuilder(builder: (context) { + result = useMemoized(valueBuilder, ['foo']); + return Container(); + }), + ); expect(result, 1); - verify(valueBuilder.call()).called(1); + verify(valueBuilder()).called(1); verifyNoMoreInteractions(valueBuilder); /* No change */ - await tester.pumpWidget(HookBuilder(builder: builder.call)); + await tester.pumpWidget( + HookBuilder(builder: (context) { + result = useMemoized(valueBuilder, ['foo']); + return Container(); + }), + ); verifyNoMoreInteractions(valueBuilder); expect(result, 1); /* Remove parameter */ - when(parameterBuilder.call()).thenReturn([]); - when(valueBuilder.call()).thenReturn(2); + when(valueBuilder()).thenReturn(2); - await tester.pumpWidget(HookBuilder(builder: builder.call)); + await tester.pumpWidget( + HookBuilder(builder: (context) { + result = useMemoized(valueBuilder, []); + return Container(); + }), + ); expect(result, 2); - verify(valueBuilder.call()).called(1); + verify(valueBuilder()).called(1); verifyNoMoreInteractions(valueBuilder); /* No change */ - await tester.pumpWidget(HookBuilder(builder: builder.call)); + await tester.pumpWidget( + HookBuilder(builder: (context) { + result = useMemoized(valueBuilder, []); + return Container(); + }), + ); verifyNoMoreInteractions(valueBuilder); expect(result, 2); @@ -127,65 +155,83 @@ void main() { testWidgets('memoized parameters compared in order', (tester) async { int result; - when(builder.call(any)).thenAnswer((invocation) { - result = useMemoized(valueBuilder.call, parameterBuilder.call()); - return Container(); - }); - - when(valueBuilder.call()).thenReturn(0); - when(parameterBuilder.call()).thenReturn(['foo', 42, 24.0]); + when(valueBuilder()).thenReturn(0); - await tester.pumpWidget(HookBuilder(builder: builder.call)); + await tester.pumpWidget( + HookBuilder(builder: (context) { + result = useMemoized(valueBuilder, ['foo', 42, 24.0]); + return Container(); + }), + ); - verify(valueBuilder.call()).called(1); + verify(valueBuilder()).called(1); verifyNoMoreInteractions(valueBuilder); expect(result, 0); /* Array reference changed but content didn't */ - when(parameterBuilder.call()).thenReturn(['foo', 42, 24.0]); - await tester.pumpWidget(HookBuilder(builder: builder.call)); + await tester.pumpWidget( + HookBuilder(builder: (context) { + result = useMemoized(valueBuilder, ['foo', 42, 24.0]); + return Container(); + }), + ); verifyNoMoreInteractions(valueBuilder); expect(result, 0); /* reoder */ - when(valueBuilder.call()).thenReturn(1); - when(parameterBuilder.call()).thenReturn([42, 'foo', 24.0]); + when(valueBuilder()).thenReturn(1); - await tester.pumpWidget(HookBuilder(builder: builder.call)); + await tester.pumpWidget( + HookBuilder(builder: (context) { + result = useMemoized(valueBuilder, [42, 'foo', 24.0]); + return Container(); + }), + ); - verify(valueBuilder.call()).called(1); + verify(valueBuilder()).called(1); verifyNoMoreInteractions(valueBuilder); expect(result, 1); - when(valueBuilder.call()).thenReturn(2); - when(parameterBuilder.call()).thenReturn([42, 24.0, 'foo']); + when(valueBuilder()).thenReturn(2); - await tester.pumpWidget(HookBuilder(builder: builder.call)); + await tester.pumpWidget( + HookBuilder(builder: (context) { + result = useMemoized(valueBuilder, [42, 24.0, 'foo']); + return Container(); + }), + ); - verify(valueBuilder.call()).called(1); + verify(valueBuilder()).called(1); verifyNoMoreInteractions(valueBuilder); expect(result, 2); /* value change */ - when(valueBuilder.call()).thenReturn(3); - when(parameterBuilder.call()).thenReturn([43, 24.0, 'foo']); + when(valueBuilder()).thenReturn(3); - await tester.pumpWidget(HookBuilder(builder: builder.call)); + await tester.pumpWidget( + HookBuilder(builder: (context) { + result = useMemoized(valueBuilder, [43, 24.0, 'foo']); + return Container(); + }), + ); - verify(valueBuilder.call()).called(1); + verify(valueBuilder()).called(1); verifyNoMoreInteractions(valueBuilder); expect(result, 3); /* Comparison is done using operator== */ // type change - when(parameterBuilder.call()).thenReturn([43.0, 24.0, 'foo']); - - await tester.pumpWidget(HookBuilder(builder: builder.call)); + await tester.pumpWidget( + HookBuilder(builder: (context) { + result = useMemoized(valueBuilder, [43, 24.0, 'foo']); + return Container(); + }), + ); verifyNoMoreInteractions(valueBuilder); expect(result, 3); @@ -203,24 +249,28 @@ void main() { int result; final parameters = []; - when(builder.call(any)).thenAnswer((invocation) { - result = useMemoized(valueBuilder.call, parameterBuilder.call()); - return Container(); - }); - - when(valueBuilder.call()).thenReturn(0); - when(parameterBuilder.call()).thenReturn(parameters); + when(valueBuilder()).thenReturn(0); - await tester.pumpWidget(HookBuilder(builder: builder.call)); + await tester.pumpWidget( + HookBuilder(builder: (context) { + result = useMemoized(valueBuilder, parameters); + return Container(); + }), + ); - verify(valueBuilder.call()).called(1); + verify(valueBuilder()).called(1); verifyNoMoreInteractions(valueBuilder); expect(result, 0); /* Array content but reference didn't */ parameters.add(42); - await tester.pumpWidget(HookBuilder(builder: builder.call)); + await tester.pumpWidget( + HookBuilder(builder: (context) { + result = useMemoized(valueBuilder, parameters); + return Container(); + }), + ); verifyNoMoreInteractions(valueBuilder); @@ -231,3 +281,7 @@ void main() { verifyNoMoreInteractions(valueBuilder); }); } + +class MockValueBuilder extends Mock { + int call(); +} diff --git a/test/mock.dart b/test/mock.dart index a2e449cd..f128f79d 100644 --- a/test/mock.dart +++ b/test/mock.dart @@ -9,24 +9,6 @@ export 'package:flutter_test/flutter_test.dart' hide Func0, Func1, Func2, Func3, Func4, Func5, Func6; export 'package:mockito/mockito.dart'; -abstract class _Func0 { - R call(); -} - -class Func0 extends Mock implements _Func0 {} - -abstract class _Func1 { - R call(T1 value); -} - -class Func1 extends Mock implements _Func1 {} - -abstract class _Func2 { - R call(T1 value, T2 value2); -} - -class Func2 extends Mock implements _Func2 {} - class HookTest extends Hook { // ignore: prefer_const_constructors_in_immutables HookTest({ @@ -79,14 +61,6 @@ class HookStateTest extends HookState> { } } - @override - void didBuild() { - super.didBuild(); - if (hook.didBuild != null) { - hook.didBuild(); - } - } - @override void reassemble() { super.reassemble(); @@ -129,28 +103,42 @@ void hotReload(WidgetTester tester) { TestWidgetsFlutterBinding.ensureInitialized().buildOwner.reassemble(root); } -Future expectPump( - Future Function() pump, - dynamic matcher, { - String reason, - dynamic skip, -}) async { - FlutterErrorDetails details; - if (skip == null || skip != false) { - final previousErrorHandler = FlutterError.onError; - FlutterError.onError = (d) { - details = d; - }; - await pump(); - FlutterError.onError = previousErrorHandler; - } +class MockSetState extends Mock { + void call(); +} + +class MockInitHook extends Mock { + void call(); +} + +class MockCreateState> extends Mock { + T call(); +} + +class MockBuild extends Mock { + T call(BuildContext context); +} + +class MockDeactivate extends Mock { + void call(); +} + +class MockErrorBuilder extends Mock { + Widget call(FlutterErrorDetails error); +} + +class MockOnError extends Mock { + void call(FlutterErrorDetails error); +} + +class MockReassemble extends Mock { + void call(); +} + +class MockDidUpdateHook extends Mock { + void call(HookTest hook); +} - await expectLater( - details != null - ? Future.error(details.exception, details.stack) - : Future.value(), - matcher, - reason: reason, - skip: skip, - ); +class MockDispose extends Mock { + void call(); } diff --git a/test/use_animation_controller_test.dart b/test/use_animation_controller_test.dart index 0f3ddd39..d950f84b 100644 --- a/test/use_animation_controller_test.dart +++ b/test/use_animation_controller_test.dart @@ -100,8 +100,7 @@ void main() { await tester.pumpWidget(const SizedBox()); }); - testWidgets('switch between controlled and uncontrolled throws', - (tester) async { + testWidgets('switch from uncontrolled to controlled throws', (tester) async { await tester.pumpWidget(HookBuilder( builder: (context) { useAnimationController(); @@ -109,19 +108,16 @@ void main() { }, )); - await expectPump( - () => tester.pumpWidget(HookBuilder( - builder: (context) { - useAnimationController(vsync: tester); - return Container(); - }, - )), - throwsAssertionError, - ); - - await tester.pumpWidget(Container()); + await tester.pumpWidget(HookBuilder( + builder: (context) { + useAnimationController(vsync: tester); + return Container(); + }, + )); - // the other way around + expect(tester.takeException(), isStateError); + }); + testWidgets('switch from controlled to uncontrolled throws', (tester) async { await tester.pumpWidget(HookBuilder( builder: (context) { useAnimationController(vsync: tester); @@ -129,15 +125,14 @@ void main() { }, )); - await expectPump( - () => tester.pumpWidget(HookBuilder( - builder: (context) { - useAnimationController(); - return Container(); - }, - )), - throwsAssertionError, - ); + await tester.pumpWidget(HookBuilder( + builder: (context) { + useAnimationController(); + return Container(); + }, + )); + + expect(tester.takeException(), isStateError); }); testWidgets('useAnimationController pass down keys', (tester) async { diff --git a/test/use_animation_test.dart b/test/use_animation_test.dart index 8b037b30..8e29ff50 100644 --- a/test/use_animation_test.dart +++ b/test/use_animation_test.dart @@ -5,14 +5,14 @@ import 'mock.dart'; void main() { testWidgets('useAnimation throws with null', (tester) async { - await expectPump( - () => tester.pumpWidget(HookBuilder( - builder: (context) { - useAnimation(null); - return Container(); - }, - )), - throwsAssertionError); + await tester.pumpWidget(HookBuilder( + builder: (context) { + useAnimation(null); + return Container(); + }, + )); + + expect(tester.takeException(), isAssertionError); }); testWidgets('useAnimation', (tester) async { diff --git a/test/use_effect_test.dart b/test/use_effect_test.dart index 670ecc68..0a7c50eb 100644 --- a/test/use_effect_test.dart +++ b/test/use_effect_test.dart @@ -4,15 +4,17 @@ import 'package:flutter_hooks/flutter_hooks.dart'; import 'mock.dart'; void main() { - final effect = Func0(); - final unrelated = Func0(); + final effect = MockEffect(); + final unrelated = MockWidgetBuild(); List parameters; - Widget builder() => HookBuilder(builder: (context) { - useEffect(effect.call, parameters); - unrelated.call(); - return Container(); - }); + Widget builder() { + return HookBuilder(builder: (context) { + useEffect(effect, parameters); + unrelated(); + return Container(); + }); + } tearDown(() { parameters = null; @@ -20,25 +22,25 @@ void main() { reset(effect); }); testWidgets('useEffect null callback throws', (tester) async { - await expectPump( - () => tester.pumpWidget(HookBuilder(builder: (c) { + await tester.pumpWidget( + HookBuilder(builder: (c) { useEffect(null); return Container(); - })), - throwsAssertionError, + }), ); + + expect(tester.takeException(), isAssertionError); }); testWidgets('useEffect calls callback on every build', (tester) async { - final effect = Func0(); - final unrelated = Func0(); + final effect = MockEffect(); + final dispose = MockDispose(); - final dispose = Func0(); - when(effect.call()).thenReturn(dispose.call); + when(effect()).thenReturn(dispose); Widget builder() { return HookBuilder(builder: (context) { - useEffect(effect.call); - unrelated.call(); + useEffect(effect); + unrelated(); return Container(); }); } @@ -46,8 +48,8 @@ void main() { await tester.pumpWidget(builder()); verifyInOrder([ - effect.call(), - unrelated.call(), + effect(), + unrelated(), ]); verifyNoMoreInteractions(dispose); verifyNoMoreInteractions(effect); @@ -55,9 +57,9 @@ void main() { await tester.pumpWidget(builder()); verifyInOrder([ - dispose.call(), - effect.call(), - unrelated.call(), + dispose(), + effect(), + unrelated(), ]); verifyNoMoreInteractions(dispose); verifyNoMoreInteractions(effect); @@ -69,8 +71,8 @@ void main() { await tester.pumpWidget(builder()); verifyInOrder([ - effect.call(), - unrelated.call(), + effect(), + unrelated(), ]); verifyNoMoreInteractions(effect); @@ -78,8 +80,8 @@ void main() { await tester.pumpWidget(builder()); verifyInOrder([ - effect.call(), - unrelated.call(), + effect(), + unrelated(), ]); verifyNoMoreInteractions(effect); }); @@ -89,8 +91,8 @@ void main() { await tester.pumpWidget(builder()); verifyInOrder([ - effect.call(), - unrelated.call(), + effect(), + unrelated(), ]); verifyNoMoreInteractions(effect); @@ -98,8 +100,8 @@ void main() { await tester.pumpWidget(builder()); verifyInOrder([ - effect.call(), - unrelated.call(), + effect(), + unrelated(), ]); verifyNoMoreInteractions(effect); }); @@ -109,8 +111,8 @@ void main() { await tester.pumpWidget(builder()); verifyInOrder([ - effect.call(), - unrelated.call(), + effect(), + unrelated(), ]); verifyNoMoreInteractions(effect); @@ -118,8 +120,8 @@ void main() { await tester.pumpWidget(builder()); verifyInOrder([ - effect.call(), - unrelated.call(), + effect(), + unrelated(), ]); verifyNoMoreInteractions(effect); }); @@ -128,8 +130,8 @@ void main() { await tester.pumpWidget(builder()); verifyInOrder([ - effect.call(), - unrelated.call(), + effect(), + unrelated(), ]); verifyNoMoreInteractions(effect); @@ -137,8 +139,8 @@ void main() { await tester.pumpWidget(builder()); verifyInOrder([ - effect.call(), - unrelated.call(), + effect(), + unrelated(), ]); verifyNoMoreInteractions(effect); }); @@ -149,8 +151,8 @@ void main() { await tester.pumpWidget(builder()); verifyInOrder([ - effect.call(), - unrelated.call(), + effect(), + unrelated(), ]); verifyNoMoreInteractions(effect); @@ -166,8 +168,8 @@ void main() { await tester.pumpWidget(builder()); verifyInOrder([ - effect.call(), - unrelated.call(), + effect(), + unrelated(), ]); verifyNoMoreInteractions(effect); @@ -179,23 +181,23 @@ void main() { testWidgets('useEffect disposer called whenever callback called', (tester) async { - final effect = Func0(); + final effect = MockEffect(); List parameters; Widget builder() { return HookBuilder(builder: (context) { - useEffect(effect.call, parameters); + useEffect(effect, parameters); return Container(); }); } parameters = ['foo']; - final disposerA = Func0(); - when(effect.call()).thenReturn(disposerA); + final disposerA = MockDispose(); + when(effect()).thenReturn(disposerA); await tester.pumpWidget(builder()); - verify(effect.call()).called(1); + verify(effect()).called(1); verifyNoMoreInteractions(effect); verifyZeroInteractions(disposerA); @@ -205,14 +207,14 @@ void main() { verifyZeroInteractions(disposerA); parameters = ['bar']; - final disposerB = Func0(); - when(effect.call()).thenReturn(disposerB); + final disposerB = MockDispose(); + when(effect()).thenReturn(disposerB); await tester.pumpWidget(builder()); verifyInOrder([ - effect.call(), - disposerA.call(), + effect(), + disposerA(), ]); verifyNoMoreInteractions(disposerA); verifyNoMoreInteractions(effect); @@ -226,9 +228,17 @@ void main() { await tester.pumpWidget(Container()); - verify(disposerB.call()).called(1); + verify(disposerB()).called(1); verifyNoMoreInteractions(disposerB); verifyNoMoreInteractions(disposerA); verifyNoMoreInteractions(effect); }); } + +class MockEffect extends Mock { + VoidCallback call(); +} + +class MockWidgetBuild extends Mock { + void call(); +} diff --git a/test/use_listenable_test.dart b/test/use_listenable_test.dart index 928cae1d..b9992611 100644 --- a/test/use_listenable_test.dart +++ b/test/use_listenable_test.dart @@ -5,14 +5,16 @@ import 'mock.dart'; void main() { testWidgets('useListenable throws with null', (tester) async { - await expectPump( - () => tester.pumpWidget(HookBuilder( - builder: (context) { - useListenable(null); - return Container(); - }, - )), - throwsAssertionError); + await tester.pumpWidget( + HookBuilder( + builder: (context) { + useListenable(null); + return Container(); + }, + ), + ); + + expect(tester.takeException(), isAssertionError); }); testWidgets('useListenable', (tester) async { var listenable = ValueNotifier(0); diff --git a/test/use_reassemble_test.dart b/test/use_reassemble_test.dart index ab79c91b..aec7c17d 100644 --- a/test/use_reassemble_test.dart +++ b/test/use_reassemble_test.dart @@ -5,17 +5,19 @@ import 'mock.dart'; void main() { testWidgets('useReassemble null callback throws', (tester) async { - await expectPump( - () => tester.pumpWidget(HookBuilder(builder: (c) { + await tester.pumpWidget( + HookBuilder(builder: (c) { useReassemble(null); return Container(); - })), - throwsAssertionError, + }), ); + + expect(tester.takeException(), isAssertionError); }); testWidgets("hot-reload calls useReassemble's callback", (tester) async { - final reassemble = Func0(); + final reassemble = MockReassemble(); + await tester.pumpWidget(HookBuilder(builder: (context) { useReassemble(reassemble); return Container(); diff --git a/test/use_reducer_test.dart b/test/use_reducer_test.dart index a54279ab..5aa34161 100644 --- a/test/use_reducer_test.dart +++ b/test/use_reducer_test.dart @@ -6,23 +6,23 @@ import 'mock.dart'; void main() { group('useReducer', () { testWidgets('basic', (tester) async { - final reducer = Func2(); + final reducer = MockReducer(); Store store; Future pump() { return tester.pumpWidget(HookBuilder( builder: (context) { - store = useReducer(reducer.call); + store = useReducer(reducer); return Container(); }, )); } - when(reducer.call(null, null)).thenReturn(0); + when(reducer(null, null)).thenReturn(0); await pump(); final element = tester.firstElement(find.byType(HookBuilder)); - verify(reducer.call(null, null)).called(1); + verify(reducer(null, null)).called(1); verifyNoMoreInteractions(reducer); expect(store.state, 0); @@ -31,87 +31,90 @@ void main() { verifyNoMoreInteractions(reducer); expect(store.state, 0); - when(reducer.call(0, 'foo')).thenReturn(1); + when(reducer(0, 'foo')).thenReturn(1); store.dispatch('foo'); - verify(reducer.call(0, 'foo')).called(1); + verify(reducer(0, 'foo')).called(1); verifyNoMoreInteractions(reducer); expect(element.dirty, true); await pump(); - when(reducer.call(1, 'bar')).thenReturn(1); + when(reducer(1, 'bar')).thenReturn(1); store.dispatch('bar'); - verify(reducer.call(1, 'bar')).called(1); + verify(reducer(1, 'bar')).called(1); verifyNoMoreInteractions(reducer); expect(element.dirty, false); }); testWidgets('reducer required', (tester) async { - await expectPump( - () => tester.pumpWidget(HookBuilder( + await tester.pumpWidget( + HookBuilder( builder: (context) { useReducer(null); return Container(); }, - )), - throwsAssertionError, + ), ); + + expect(tester.takeException(), isAssertionError); }); testWidgets('dispatch during build fails', (tester) async { - final reducer = Func2(); + final reducer = MockReducer(); - await expectPump( - () => tester.pumpWidget(HookBuilder( + await tester.pumpWidget( + HookBuilder( builder: (context) { useReducer(reducer.call).dispatch('Foo'); return Container(); }, - )), - throwsAssertionError, + ), ); + + expect(tester.takeException(), isAssertionError); }); testWidgets('first reducer call receive initialAction and initialState', (tester) async { - final reducer = Func2(); + final reducer = MockReducer(); + when(reducer(0, 'Foo')).thenReturn(42); - when(reducer.call(0, 'Foo')).thenReturn(0); - await expectPump( - () => tester.pumpWidget(HookBuilder( + await tester.pumpWidget( + HookBuilder( builder: (context) { - useReducer( - reducer.call, + final result = useReducer( + reducer, initialAction: 'Foo', initialState: 0, - ); - return Container(); + ).state; + return Text('$result', textDirection: TextDirection.ltr); }, - )), - completes, + ), ); + + expect(find.text('42'), findsOneWidget); }); testWidgets('dispatchs reducer call must not return null', (tester) async { - final reducer = Func2(); + final reducer = MockReducer(); Store store; Future pump() { return tester.pumpWidget(HookBuilder( builder: (context) { - store = useReducer(reducer.call); + store = useReducer(reducer); return Container(); }, )); } - when(reducer.call(null, null)).thenReturn(42); + when(reducer(null, null)).thenReturn(42); await pump(); - when(reducer.call(42, 'foo')).thenReturn(null); + when(reducer(42, 'foo')).thenReturn(null); expect(() => store.dispatch('foo'), throwsAssertionError); await pump(); @@ -119,17 +122,22 @@ void main() { }); testWidgets('first reducer call must not return null', (tester) async { - final reducer = Func2(); + final reducer = MockReducer(); - await expectPump( - () => tester.pumpWidget(HookBuilder( + await tester.pumpWidget( + HookBuilder( builder: (context) { useReducer(reducer.call); return Container(); }, - )), - throwsAssertionError, + ), ); + + expect(tester.takeException(), isAssertionError); }); }); } + +class MockReducer extends Mock { + int call(int state, String action); +} diff --git a/test/use_ticker_provider_test.dart b/test/use_ticker_provider_test.dart index 89c41182..be7ff0dc 100644 --- a/test/use_ticker_provider_test.dart +++ b/test/use_ticker_provider_test.dart @@ -52,10 +52,10 @@ void main() { try { animationController.forward(); - await expectPump( - () => tester.pumpWidget(const SizedBox()), - throwsFlutterError, - ); + + await tester.pumpWidget(const SizedBox()); + + expect(tester.takeException(), isFlutterError); } finally { animationController.dispose(); } diff --git a/test/use_value_changed_test.dart b/test/use_value_changed_test.dart index f99b3872..41cf0247 100644 --- a/test/use_value_changed_test.dart +++ b/test/use_value_changed_test.dart @@ -6,15 +6,18 @@ import 'mock.dart'; void main() { testWidgets('useValueChanged basic', (tester) async { var value = 42; - final _useValueChanged = Func2(); + final _useValueChanged = MockValueChanged(); String result; - Future pump() => tester.pumpWidget(HookBuilder( - builder: (context) { - result = useValueChanged(value, _useValueChanged.call); - return Container(); - }, - )); + Future pump() { + return tester.pumpWidget( + HookBuilder(builder: (context) { + result = useValueChanged(value, _useValueChanged); + return Container(); + }), + ); + } + await pump(); final context = find.byType(HookBuilder).evaluate().first; @@ -30,10 +33,10 @@ void main() { expect(context.dirty, false); value++; - when(_useValueChanged.call(any, any)).thenReturn('Hello'); + when(_useValueChanged(any, any)).thenReturn('Hello'); await pump(); - verify(_useValueChanged.call(42, null)); + verify(_useValueChanged(42, null)); expect(result, 'Hello'); verifyNoMoreInteractions(_useValueChanged); expect(context.dirty, false); @@ -45,11 +48,11 @@ void main() { expect(context.dirty, false); value++; - when(_useValueChanged.call(any, any)).thenReturn('Foo'); + when(_useValueChanged(any, any)).thenReturn('Foo'); await pump(); expect(result, 'Foo'); - verify(_useValueChanged.call(43, 'Hello')); + verify(_useValueChanged(43, 'Hello')); verifyNoMoreInteractions(_useValueChanged); expect(context.dirty, false); @@ -74,3 +77,7 @@ void main() { expect(tester.takeException(), isAssertionError); }); } + +class MockValueChanged extends Mock { + String call(int value, String previous); +} diff --git a/test/use_value_listenable_test.dart b/test/use_value_listenable_test.dart index e2e3308c..640034c4 100644 --- a/test/use_value_listenable_test.dart +++ b/test/use_value_listenable_test.dart @@ -5,14 +5,16 @@ import 'mock.dart'; void main() { testWidgets('useValueListenable throws with null', (tester) async { - await expectPump( - () => tester.pumpWidget(HookBuilder( - builder: (context) { - useValueListenable(null); - return Container(); - }, - )), - throwsAssertionError); + await tester.pumpWidget( + HookBuilder( + builder: (context) { + useValueListenable(null); + return Container(); + }, + ), + ); + + expect(tester.takeException(), isAssertionError); }); testWidgets('useValueListenable', (tester) async { var listenable = ValueNotifier(0); diff --git a/test/use_value_notifier_test.dart b/test/use_value_notifier_test.dart index 4bc660e1..1a0c4f48 100644 --- a/test/use_value_notifier_test.dart +++ b/test/use_value_notifier_test.dart @@ -8,7 +8,7 @@ void main() { testWidgets('useValueNotifier basic', (tester) async { ValueNotifier state; HookElement element; - final listener = Func0(); + final listener = MockListener(); await tester.pumpWidget(HookBuilder( builder: (context) { @@ -18,7 +18,7 @@ void main() { }, )); - state.addListener(listener.call); + state.addListener(listener); expect(state.value, 42); expect(element.dirty, false); @@ -31,7 +31,7 @@ void main() { expect(element.dirty, false); state.value++; - verify(listener.call()).called(1); + verify(listener()).called(1); verifyNoMoreInteractions(listener); expect(element.dirty, false); await tester.pump(); @@ -50,7 +50,7 @@ void main() { testWidgets('no initial data', (tester) async { ValueNotifier state; HookElement element; - final listener = Func0(); + final listener = MockListener(); await tester.pumpWidget(HookBuilder( builder: (context) { @@ -60,7 +60,7 @@ void main() { }, )); - state.addListener(listener.call); + state.addListener(listener); expect(state.value, null); expect(element.dirty, false); @@ -74,7 +74,7 @@ void main() { state.value = 43; expect(element.dirty, false); - verify(listener.call()).called(1); + verify(listener()).called(1); verifyNoMoreInteractions(listener); await tester.pump(); @@ -133,3 +133,7 @@ void main() { }); }); } + +class MockListener extends Mock { + void call(); +} From 4480b71828ca9183d179afb77b2c0362506af491 Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Sat, 4 Jul 2020 19:25:01 +0100 Subject: [PATCH 135/384] Hook.use -> use --- README.md | 2 +- lib/src/animation.dart | 4 +- lib/src/async.dart | 6 +- lib/src/focus.dart | 2 +- lib/src/listenable.dart | 4 +- lib/src/misc.dart | 6 +- lib/src/primitives.dart | 8 +- lib/src/text_controller.dart | 4 +- test/hook_widget_test.dart | 134 ++++++++++++++++----------------- test/pre_build_abort_test.dart | 14 ++-- 10 files changed, 92 insertions(+), 92 deletions(-) diff --git a/README.md b/README.md index 32e69e0b..f772cd21 100644 --- a/README.md +++ b/README.md @@ -250,7 +250,7 @@ There are two ways to create a hook: ```dart Result useMyHook(BuildContext context) { - return Hook.use(const _TimeAlive()); + return use(const _TimeAlive()); } ``` diff --git a/lib/src/animation.dart b/lib/src/animation.dart index 1cad8170..53f2bb20 100644 --- a/lib/src/animation.dart +++ b/lib/src/animation.dart @@ -35,7 +35,7 @@ AnimationController useAnimationController({ }) { vsync ??= useSingleTickerProvider(keys: keys); - return Hook.use( + return use( _AnimationControllerHook( duration: duration, debugLabel: debugLabel, @@ -120,7 +120,7 @@ class _AnimationControllerHookState /// See also: /// * [SingleTickerProviderStateMixin] TickerProvider useSingleTickerProvider({List keys}) { - return Hook.use( + return use( keys != null ? _SingleTickerProviderHook(keys) : const _SingleTickerProviderHook(), diff --git a/lib/src/async.dart b/lib/src/async.dart index ef3bc4bd..842f07ff 100644 --- a/lib/src/async.dart +++ b/lib/src/async.dart @@ -10,7 +10,7 @@ part of 'hooks.dart'; /// * [useStream], similar to [useFuture] but for [Stream]. AsyncSnapshot useFuture(Future future, {T initialData, bool preserveState = true}) { - return Hook.use(_FutureHook(future, + return use(_FutureHook(future, initialData: initialData, preserveState: preserveState)); } @@ -100,7 +100,7 @@ class _FutureStateHook extends HookState, _FutureHook> { /// * [useFuture], similar to [useStream] but for [Future]. AsyncSnapshot useStream(Stream stream, {T initialData, bool preserveState = true}) { - return Hook.use(_StreamHook( + return use(_StreamHook( stream, initialData: initialData, preserveState: preserveState, @@ -213,7 +213,7 @@ StreamController useStreamController( VoidCallback onListen, VoidCallback onCancel, List keys}) { - return Hook.use(_StreamControllerHook( + return use(_StreamControllerHook( onCancel: onCancel, onListen: onListen, sync: sync, diff --git a/lib/src/focus.dart b/lib/src/focus.dart index db64d1f0..b3dbcbaa 100644 --- a/lib/src/focus.dart +++ b/lib/src/focus.dart @@ -4,7 +4,7 @@ part of 'hooks.dart'; /// /// See also: /// - [FocusNode] -FocusNode useFocusNode() => Hook.use(const _FocusNodeHook()); +FocusNode useFocusNode() => use(const _FocusNodeHook()); class _FocusNodeHook extends Hook { const _FocusNodeHook(); diff --git a/lib/src/listenable.dart b/lib/src/listenable.dart index f46b419a..fc678fef 100644 --- a/lib/src/listenable.dart +++ b/lib/src/listenable.dart @@ -16,7 +16,7 @@ T useValueListenable(ValueListenable valueListenable) { /// * [Listenable] /// * [useValueListenable], [useAnimation] T useListenable(T listenable) { - Hook.use(_ListenableHook(listenable)); + use(_ListenableHook(listenable)); return listenable; } @@ -68,7 +68,7 @@ class _ListenableStateHook extends HookState { /// * [ValueNotifier] /// * [useValueListenable] ValueNotifier useValueNotifier([T intialData, List keys]) { - return Hook.use(_ValueNotifierHook( + return use(_ValueNotifierHook( initialData: intialData, keys: keys, )); diff --git a/lib/src/misc.dart b/lib/src/misc.dart index ea293f55..24fce447 100644 --- a/lib/src/misc.dart +++ b/lib/src/misc.dart @@ -38,7 +38,7 @@ Store useReducer( State initialState, Action initialAction, }) { - return Hook.use(_ReducerdHook(reducer, + return use(_ReducerdHook(reducer, initialAction: initialAction, initialState: initialState)); } @@ -86,7 +86,7 @@ class _ReducerdHookState /// Returns the previous argument called to [usePrevious]. T usePrevious(T val) { - return Hook.use(_PreviousHook(val)); + return use(_PreviousHook(val)); } class _PreviousHook extends Hook { @@ -118,7 +118,7 @@ class _PreviousHookState extends HookState> { /// * [State.reassemble] void useReassemble(VoidCallback callback) { assert(() { - Hook.use(_ReassembleHook(callback)); + use(_ReassembleHook(callback)); return true; }(), ''); } diff --git a/lib/src/primitives.dart b/lib/src/primitives.dart index 6ea73a60..d7ff2017 100644 --- a/lib/src/primitives.dart +++ b/lib/src/primitives.dart @@ -8,7 +8,7 @@ part of 'hooks.dart'; /// A later call of [useMemoized] with different [keys] will call [useMemoized] again to create a new instance. T useMemoized(T Function() valueBuilder, [List keys = const []]) { - return Hook.use(_MemoizedHook( + return use(_MemoizedHook( valueBuilder, keys: keys, )); @@ -66,7 +66,7 @@ R useValueChanged( T value, R Function(T oldValue, R oldResult) valueChange, ) { - return Hook.use(_ValueChangedHook(value, valueChange)); + return use(_ValueChangedHook(value, valueChange)); } class _ValueChangedHook extends Hook { @@ -128,7 +128,7 @@ typedef Dispose = void Function(); /// ); /// ``` void useEffect(Dispose Function() effect, [List keys]) { - Hook.use(_EffectHook(effect, keys)); + use(_EffectHook(effect, keys)); } class _EffectHook extends Hook { @@ -207,7 +207,7 @@ class _EffectHookState extends HookState { /// * [ValueNotifier] /// * [useStreamController], an alternative to [ValueNotifier] for state. ValueNotifier useState([T initialData]) { - return Hook.use(_StateHook(initialData: initialData)); + return use(_StateHook(initialData: initialData)); } class _StateHook extends Hook> { diff --git a/lib/src/text_controller.dart b/lib/src/text_controller.dart index 3c6828ba..6bbd7dd9 100644 --- a/lib/src/text_controller.dart +++ b/lib/src/text_controller.dart @@ -8,13 +8,13 @@ class _TextEditingControllerHookCreator { /// The [text] parameter can be used to set the initial value of the /// controller. TextEditingController call({String text, List keys}) { - return Hook.use(_TextEditingControllerHook(text, null, keys)); + return use(_TextEditingControllerHook(text, null, keys)); } /// Creates a [TextEditingController] from the initial [value] that will /// be disposed automatically. TextEditingController fromValue(TextEditingValue value, [List keys]) { - return Hook.use(_TextEditingControllerHook(null, value, keys)); + return use(_TextEditingControllerHook(null, value, keys)); } } diff --git a/test/hook_widget_test.dart b/test/hook_widget_test.dart index b54b543e..2ef9912f 100644 --- a/test/hook_widget_test.dart +++ b/test/hook_widget_test.dart @@ -157,7 +157,7 @@ void main() { child: HookBuilder( key: value ? _key2 : _key1, builder: (context) { - Hook.use(HookTest(deactivate: deactivate1)); + use(HookTest(deactivate: deactivate1)); return Container(); }, ), @@ -165,7 +165,7 @@ void main() { HookBuilder( key: !value ? _key2 : _key1, builder: (context) { - Hook.use(HookTest(deactivate: deactivate2)); + use(HookTest(deactivate: deactivate2)); return Container(); }, ), @@ -213,8 +213,8 @@ void main() { final widget = HookBuilder( key: _key, builder: (context) { - Hook.use(HookTest(deactivate: deactivate)); - Hook.use(HookTest(deactivate: deactivate2)); + use(HookTest(deactivate: deactivate)); + use(HookTest(deactivate: deactivate2)); return Container(); }, ); @@ -249,7 +249,7 @@ void main() { (tester) async { await tester.pumpWidget( HookBuilder(builder: (_) { - Hook.use(InheritedInitHook()); + use(InheritedInitHook()); return Container(); }), ); @@ -267,7 +267,7 @@ void main() { await tester.pumpWidget( HookBuilder(builder: (_) { - Hook.use(HookTest(build: build)); + use(HookTest(build: build)); return Container(); }), ); @@ -297,8 +297,8 @@ void main() { testWidgets('HookElement exposes an immutable list of hooks', (tester) async { await tester.pumpWidget( HookBuilder(builder: (_) { - Hook.use(HookTest()); - Hook.use(HookTest()); + use(HookTest()); + use(HookTest()); return Container(); }), ); @@ -322,7 +322,7 @@ void main() { await tester.pumpWidget( HookBuilder(builder: (_) { - Hook.use(HookTest()); + use(HookTest()); throw 1; }), ); @@ -330,8 +330,8 @@ void main() { await tester.pumpWidget( HookBuilder(builder: (_) { - Hook.use(HookTest()); - Hook.use(HookTest()); + use(HookTest()); + use(HookTest()); throw 2; }), ); @@ -339,9 +339,9 @@ void main() { await tester.pumpWidget( HookBuilder(builder: (_) { - Hook.use(HookTest()); - Hook.use(HookTest()); - Hook.use(HookTest()); + use(HookTest()); + use(HookTest()); + use(HookTest()); return Container(); }), ); @@ -358,7 +358,7 @@ void main() { await tester.pumpWidget( HookBuilder(builder: (_) { - Hook.use(HookTest()); + use(HookTest()); throw 1; }), ); @@ -366,9 +366,9 @@ void main() { await tester.pumpWidget( HookBuilder(builder: (_) { - Hook.use(HookTest()); - Hook.use(HookTest()); - Hook.use(HookTest()); + use(HookTest()); + use(HookTest()); + use(HookTest()); throw 2; }), ); @@ -395,7 +395,7 @@ void main() { await tester.pumpWidget( HookBuilder(builder: (_) { - Hook.use(HookTest()); + use(HookTest()); return Container(); }), ); @@ -406,7 +406,7 @@ void main() { (tester) async { await tester.pumpWidget( HookBuilder(builder: (_) { - Hook.use(createHook()); + use(createHook()); return Container(); }), ); @@ -438,8 +438,8 @@ void main() { await tester.pumpWidget( HookBuilder(builder: (context) { - Hook.use(HookTest(dispose: dispose)); - Hook.use(HookTest(dispose: dispose2)); + use(HookTest(dispose: dispose)); + use(HookTest(dispose: dispose2)); return Container(); }), ); @@ -449,8 +449,8 @@ void main() { await tester.pumpWidget( HookBuilder(builder: (context) { - Hook.use(HookTest(dispose: dispose, keys: const [])); - Hook.use(HookTest(dispose: dispose2)); + use(HookTest(dispose: dispose, keys: const [])); + use(HookTest(dispose: dispose2)); return Container(); }), ); @@ -460,8 +460,8 @@ void main() { await tester.pumpWidget( HookBuilder(builder: (context) { - Hook.use(HookTest(dispose: dispose, keys: const [])); - Hook.use(HookTest(dispose: dispose2, keys: const [])); + use(HookTest(dispose: dispose, keys: const [])); + use(HookTest(dispose: dispose2, keys: const [])); return Container(); }), ); @@ -477,7 +477,7 @@ void main() { Widget $build() { return HookBuilder(builder: (context) { - Hook.use( + use( HookTest( build: build, dispose: dispose, @@ -567,7 +567,7 @@ void main() { await tester.pumpWidget(HookBuilder( builder: (context) { hookContext = context as HookElement; - state = Hook.use(hook); + state = use(hook); return Container(); }, )); @@ -591,7 +591,7 @@ void main() { await tester.pumpWidget(HookBuilder( builder: (context) { hook = createHook(); - result = Hook.use(hook); + result = use(hook); return Container(); }, )); @@ -610,7 +610,7 @@ void main() { await tester.pumpWidget(HookBuilder( builder: (context) { hook = createHook(); - result = Hook.use(hook); + result = use(hook); return Container(); }, )); @@ -640,8 +640,8 @@ void main() { await tester.pumpWidget( HookBuilder(builder: (context) { - Hook.use(createHook()); - Hook.use(HookTest(dispose: dispose2)); + use(createHook()); + use(HookTest(dispose: dispose2)); return Container(); }), ); @@ -663,7 +663,7 @@ void main() { await tester.pumpWidget( HookBuilder(builder: (context) { - Hook.use(hook); + use(hook); return Container(); }), ); @@ -677,7 +677,7 @@ void main() { await tester.pumpWidget( HookBuilder(builder: (context) { - Hook.use(hook); + use(hook); return Container(); }), ); @@ -693,14 +693,14 @@ void main() { testWidgets('rebuild with different hooks crash', (tester) async { await tester.pumpWidget( HookBuilder(builder: (context) { - Hook.use(HookTest()); + use(HookTest()); return Container(); }), ); await tester.pumpWidget( HookBuilder(builder: (context) { - Hook.use(HookTest()); + use(HookTest()); return Container(); }), ); @@ -758,7 +758,7 @@ void main() { }), ); - expect(() => Hook.use(HookTest()), throwsAssertionError); + expect(() => use(HookTest()), throwsAssertionError); }); testWidgets('hot-reload triggers a build', (tester) async { @@ -770,7 +770,7 @@ void main() { await tester.pumpWidget( HookBuilder(builder: (context) { previousHook = createHook(); - result = Hook.use(previousHook); + result = use(previousHook); return Container(); }), ); @@ -802,8 +802,8 @@ void main() { final didUpdateHook2 = MockDidUpdateHook(); await tester.pumpWidget( HookBuilder(builder: (context) { - Hook.use(createHook()); - Hook.use( + use(createHook()); + use( HookTest( reassemble: reassemble2, didUpdateHook: didUpdateHook2, @@ -830,7 +830,7 @@ void main() { testWidgets("hot-reload don't reassemble newly added hooks", (tester) async { await tester.pumpWidget( HookBuilder(builder: (context) { - Hook.use(HookTest()); + use(HookTest()); return Container(); }), ); @@ -840,8 +840,8 @@ void main() { hotReload(tester); await tester.pumpWidget( HookBuilder(builder: (context) { - Hook.use(HookTest()); - Hook.use(createHook()); + use(HookTest()); + use(createHook()); return Container(); }), ); @@ -861,7 +861,7 @@ void main() { await tester.pumpWidget( HookBuilder(builder: (context) { - Hook.use(hook1 = createHook()); + use(hook1 = createHook()); return Container(); }), ); @@ -879,8 +879,8 @@ void main() { await tester.pumpWidget( HookBuilder(builder: (context) { - Hook.use(createHook()); - Hook.use( + use(createHook()); + use( HookTest( initHook: initHook2, build: build2, @@ -913,7 +913,7 @@ void main() { await tester.pumpWidget( HookBuilder(builder: (context) { - Hook.use(createHook()); + use(createHook()); return Container(); }), ); @@ -931,13 +931,13 @@ void main() { await tester.pumpWidget( HookBuilder(builder: (context) { - Hook.use(HookTest( + use(HookTest( initHook: initHook2, build: build2, didUpdateHook: didUpdateHook2, dispose: dispose2, )); - Hook.use(createHook()); + use(createHook()); return Container(); }), ); @@ -962,8 +962,8 @@ void main() { await tester.pumpWidget( HookBuilder(builder: (context) { - Hook.use(createHook()); - Hook.use( + use(createHook()); + use( HookTest( initHook: initHook2, build: build2, @@ -1029,10 +1029,10 @@ void main() { await tester.pumpWidget( HookBuilder(builder: (context) { - Hook.use(hook1 = createHook()); - Hook.use(HookTest(dispose: dispose2)); - Hook.use(HookTest(dispose: dispose3)); - Hook.use(HookTest(dispose: dispose4)); + use(hook1 = createHook()); + use(HookTest(dispose: dispose2)); + use(HookTest(dispose: dispose3)); + use(HookTest(dispose: dispose4)); return Container(); }), ); @@ -1064,23 +1064,23 @@ void main() { await tester.pumpWidget( HookBuilder(builder: (context) { - Hook.use(createHook()); + use(createHook()); // changed type from HookTest - Hook.use( + use( HookTest( initHook: initHook2, build: build2, didUpdateHook: didUpdateHook2, ), ); - Hook.use( + use( HookTest( initHook: initHook3, build: build3, didUpdateHook: didUpdateHook3, ), ); - Hook.use( + use( HookTest( initHook: initHook4, build: build4, @@ -1131,10 +1131,10 @@ void main() { await tester.pumpWidget( HookBuilder(builder: (context) { - Hook.use(hook1 = createHook()); - Hook.use(HookTest(dispose: dispose2)); - Hook.use(HookTest(dispose: dispose3)); - Hook.use(HookTest(dispose: dispose4)); + use(hook1 = createHook()); + use(HookTest(dispose: dispose2)); + use(HookTest(dispose: dispose3)); + use(HookTest(dispose: dispose4)); return Container(); }), ); @@ -1165,19 +1165,19 @@ void main() { hotReload(tester); await tester.pumpWidget( HookBuilder(builder: (context) { - Hook.use(createHook()); + use(createHook()); // changed type from HookTest - Hook.use(HookTest( + use(HookTest( initHook: initHook2, build: build2, didUpdateHook: didUpdateHook2, )); - Hook.use(HookTest( + use(HookTest( initHook: initHook3, build: build3, didUpdateHook: didUpdateHook3, )); - Hook.use(HookTest( + use(HookTest( initHook: initHook4, build: build4, didUpdateHook: didUpdateHook4, diff --git a/test/pre_build_abort_test.dart b/test/pre_build_abort_test.dart index c076e0dd..55deeceb 100644 --- a/test/pre_build_abort_test.dart +++ b/test/pre_build_abort_test.dart @@ -13,7 +13,7 @@ void main() { await tester.pumpWidget( HookBuilder(builder: (c) { buildCount++; - first = Hook.use(const MayRebuild()); + first = use(const MayRebuild()); return Container(); }), @@ -36,8 +36,8 @@ void main() { await tester.pumpWidget( HookBuilder(builder: (c) { buildCount++; - first = Hook.use(MayRebuild(firstSpy)); - second = Hook.use(MayRebuild(secondSpy)); + first = use(MayRebuild(firstSpy)); + second = use(MayRebuild(secondSpy)); return Container(); }), @@ -81,7 +81,7 @@ void main() { await tester.pumpWidget( HookBuilder(builder: (c) { buildCount++; - final value = Hook.use(IsPositiveHook(notifier)); + final value = use(IsPositiveHook(notifier)); return Text('$value', textDirection: TextDirection.ltr); }), @@ -124,7 +124,7 @@ void main() { HookBuilder(builder: (c) { buildCount++; useListenable(notifier); - final value = Hook.use(IsPositiveHook(notifier)); + final value = use(IsPositiveHook(notifier)); return Text('$value', textDirection: TextDirection.ltr); }), @@ -149,7 +149,7 @@ void main() { Widget build() { return HookBuilder(builder: (c) { buildCount++; - final value = Hook.use(IsPositiveHook(notifier)); + final value = use(IsPositiveHook(notifier)); return Text('$value', textDirection: TextDirection.ltr); }); @@ -176,7 +176,7 @@ void main() { final child = HookBuilder(builder: (c) { buildCount++; Directionality.of(c); - final value = Hook.use(IsPositiveHook(notifier)); + final value = use(IsPositiveHook(notifier)); return Text('$value', textDirection: TextDirection.ltr); }); From 4a067e5006e15715454b206c0d750ba9eb07095f Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Sat, 4 Jul 2020 19:26:21 +0100 Subject: [PATCH 136/384] Changelog --- CHANGELOG.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c44a5491..f1056df2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,19 @@ } ``` +- Deprecated `Hook.use` in favor of a new short-hand `use`. + Before: + + ```dart + Hook.use(MyHook()); + ``` + + After: + + ```dart + use(MyHook()); + ``` + ## 0.10.0 **Breaking change**: From b633e85a0f55947e79343cb0d2032b295cced240 Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Sat, 4 Jul 2020 19:30:17 +0100 Subject: [PATCH 137/384] Fix broken link --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f772cd21..d4c6dc55 100644 --- a/README.md +++ b/README.md @@ -339,7 +339,7 @@ A series of hooks with no particular theme. | ----------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------ | | [useReducer](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useReducer.html) | An alternative to `useState` for more complex states. | | [usePrevious](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/usePrevious.html) | Returns the previous argument called to [usePrevious]. | -| [useTextEditingController](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useTextEditingController.html) | Create a `TextEditingController` | +| [useTextEditingController](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useTextEditingController-constant.html) | Create a `TextEditingController` | | [useFocusNode](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useFocusNode.html) | Create a `FocusNode` | ## Contributions From 159dd687e1f4455c5d561cf4b551e84d9ffed32e Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Sat, 4 Jul 2020 19:41:28 +0100 Subject: [PATCH 138/384] coverage --- lib/src/framework.dart | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/lib/src/framework.dart b/lib/src/framework.dart index 7b945884..71ee4ec0 100644 --- a/lib/src/framework.dart +++ b/lib/src/framework.dart @@ -15,15 +15,8 @@ bool debugHotReloadHooksEnabled = true; /// and all calls to [use] must be made unconditionally, always on the same order. /// /// See [Hook] for more explanations. -R use(Hook hook) { - assert(HookElement._currentHookElement != null, ''' -Hooks can only be called from the build method of a widget that mix-in `Hooks`. - -Hooks should only be called within the build method of a widget. -Calling them outside of build method leads to an unstable state and is therefore prohibited. -'''); - return HookElement._currentHookElement._use(hook); -} +// ignore: deprecated_member_use_from_same_package +R use(Hook hook) => Hook.use(hook); /// [Hook] is similar to a [StatelessWidget], but is not associated /// to an [Element]. From 4cd7069bd7b5fd49382a427aadc500d34030ae02 Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Sat, 4 Jul 2020 19:52:59 +0100 Subject: [PATCH 139/384] v0.11.0 --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index d172ecbe..99ea00c8 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -3,7 +3,7 @@ description: A flutter implementation of React hooks. It adds a new kind of widg homepage: https://github.com/rrousselGit/flutter_hooks author: Remi Rousselet -version: 0.10.0 +version: 0.11.0 environment: sdk: ">=2.7.0 <3.0.0" From 36741f7e9c36822f5c4e734e3fd4c89b0af883b5 Mon Sep 17 00:00:00 2001 From: Albert Wolszon Date: Mon, 6 Jul 2020 15:22:21 +0200 Subject: [PATCH 140/384] Add useTabController hook --- README.md | 1 + lib/src/hooks.dart | 2 ++ lib/src/tab_controller.dart | 56 +++++++++++++++++++++++++++++++ test/use_tab_controller_test.dart | 52 ++++++++++++++++++++++++++++ 4 files changed, 111 insertions(+) create mode 100644 lib/src/tab_controller.dart create mode 100644 test/use_tab_controller_test.dart diff --git a/README.md b/README.md index d4c6dc55..e74cf763 100644 --- a/README.md +++ b/README.md @@ -341,6 +341,7 @@ A series of hooks with no particular theme. | [usePrevious](https://pub.dartlang.org/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) | Create a `TextEditingController` | | [useFocusNode](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useFocusNode.html) | Create a `FocusNode` | +| [useTabController](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useTabController.html) | Creates and disposes a `TabController`. | ## Contributions diff --git a/lib/src/hooks.dart b/lib/src/hooks.dart index 45cb6731..1452b76b 100644 --- a/lib/src/hooks.dart +++ b/lib/src/hooks.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart' show TabController; import 'package:flutter/scheduler.dart'; import 'package:flutter/widgets.dart'; @@ -11,5 +12,6 @@ part 'async.dart'; part 'listenable.dart'; part 'misc.dart'; part 'primitives.dart'; +part 'tab_controller.dart'; part 'text_controller.dart'; part 'focus.dart'; diff --git a/lib/src/tab_controller.dart b/lib/src/tab_controller.dart new file mode 100644 index 00000000..a6ca6033 --- /dev/null +++ b/lib/src/tab_controller.dart @@ -0,0 +1,56 @@ +part of 'hooks.dart'; + +/// Creates and disposes a [TabController]. +/// +/// See also: +/// - [TabController] +TabController useTabController({ + @required TickerProvider vsync, + @required int length, + int initialIndex = 0, + List keys, +}) { + return use(_TabControllerHook( + vsync: vsync, + length: length, + initialIndex: initialIndex, + keys: keys, + )); +} + +class _TabControllerHook extends Hook { + const _TabControllerHook({ + @required this.vsync, + @required this.length, + this.initialIndex = 0, + List keys, + }) : super(keys: keys); + + final TickerProvider vsync; + final int length; + final int initialIndex; + + @override + HookState> createState() => + _TabControllerHookState(); +} + +class _TabControllerHookState + extends HookState { + TabController controller; + + @override + void initHook() { + controller = TabController( + length: hook.length, + initialIndex: hook.initialIndex, + vsync: hook.vsync, + ); + } + + @override + TabController build(BuildContext context) => controller; + + @override + void dispose() => controller?.dispose(); +} diff --git a/test/use_tab_controller_test.dart b/test/use_tab_controller_test.dart new file mode 100644 index 00000000..10183255 --- /dev/null +++ b/test/use_tab_controller_test.dart @@ -0,0 +1,52 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:flutter_hooks/src/framework.dart'; +import 'package:flutter_hooks/src/hooks.dart'; + +void main() { + group('useTabController', () { + testWidgets("returns a TabController that doesn't change", (tester) async { + final rebuilder = ValueNotifier(0); + TabController controller; + + await tester.pumpWidget(HookBuilder( + builder: (context) { + final ticker = useSingleTickerProvider(); + controller = useTabController(length: 1, vsync: ticker); + + useValueNotifier(rebuilder); + + return const SizedBox(); + }, + )); + + expect(controller, isA()); + + final oldController = controller; + rebuilder.notifyListeners(); + await tester.pumpAndSettle(); + + expect(identical(controller, oldController), isTrue); + }); + + testWidgets('passes hook parameters to the TabController', (tester) async { + TabController controller; + + await tester.pumpWidget(HookBuilder( + builder: (context) { + final ticker = useSingleTickerProvider(); + controller = useTabController( + initialIndex: 2, + length: 4, + vsync: ticker, + ); + + return const SizedBox(); + }, + )); + + expect(controller.index, 2); + expect(controller.length, 4); + }); + }); +} From 2189296750c5c9984018d549e498996c37b4ed9d Mon Sep 17 00:00:00 2001 From: Albert221 Date: Tue, 7 Jul 2020 07:58:40 +0200 Subject: [PATCH 141/384] Make vsync optional --- lib/src/tab_controller.dart | 8 +++++--- test/use_tab_controller_test.dart | 5 +---- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/lib/src/tab_controller.dart b/lib/src/tab_controller.dart index a6ca6033..a443b86b 100644 --- a/lib/src/tab_controller.dart +++ b/lib/src/tab_controller.dart @@ -5,11 +5,13 @@ part of 'hooks.dart'; /// See also: /// - [TabController] TabController useTabController({ - @required TickerProvider vsync, @required int length, + TickerProvider vsync, int initialIndex = 0, List keys, }) { + vsync ??= useSingleTickerProvider(keys: keys); + return use(_TabControllerHook( vsync: vsync, length: length, @@ -20,14 +22,14 @@ TabController useTabController({ class _TabControllerHook extends Hook { const _TabControllerHook({ - @required this.vsync, @required this.length, + @required this.vsync, this.initialIndex = 0, List keys, }) : super(keys: keys); - final TickerProvider vsync; final int length; + final TickerProvider vsync; final int initialIndex; @override diff --git a/test/use_tab_controller_test.dart b/test/use_tab_controller_test.dart index 10183255..d85f38f9 100644 --- a/test/use_tab_controller_test.dart +++ b/test/use_tab_controller_test.dart @@ -11,8 +11,7 @@ void main() { await tester.pumpWidget(HookBuilder( builder: (context) { - final ticker = useSingleTickerProvider(); - controller = useTabController(length: 1, vsync: ticker); + controller = useTabController(length: 1); useValueNotifier(rebuilder); @@ -34,11 +33,9 @@ void main() { await tester.pumpWidget(HookBuilder( builder: (context) { - final ticker = useSingleTickerProvider(); controller = useTabController( initialIndex: 2, length: 4, - vsync: ticker, ); return const SizedBox(); From d0ab657dbcc8e13b1f450a724db0b6383229296c Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Tue, 7 Jul 2020 09:49:44 +0100 Subject: [PATCH 142/384] Test custom vsync --- example/.flutter-plugins-dependencies | 2 +- lib/src/tab_controller.dart | 4 +- test/use_tab_controller_test.dart | 99 +++++++++++++++++++++------ 3 files changed, 80 insertions(+), 25 deletions(-) diff --git a/example/.flutter-plugins-dependencies b/example/.flutter-plugins-dependencies index 982acf6a..32b57698 100644 --- a/example/.flutter-plugins-dependencies +++ b/example/.flutter-plugins-dependencies @@ -1 +1 @@ -{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"shared_preferences","path":"/Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/shared_preferences-0.4.3/","dependencies":[]}],"android":[{"name":"shared_preferences","path":"/Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/shared_preferences-0.4.3/","dependencies":[]}],"macos":[],"linux":[],"windows":[],"web":[]},"dependencyGraph":[{"name":"shared_preferences","dependencies":[]}],"date_created":"2020-07-02 11:47:33.991520","version":"1.20.0-3.0.pre.100"} \ No newline at end of file +{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"shared_preferences","path":"/Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/shared_preferences-0.4.3/","dependencies":[]}],"android":[{"name":"shared_preferences","path":"/Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/shared_preferences-0.4.3/","dependencies":[]}],"macos":[],"linux":[],"windows":[],"web":[]},"dependencyGraph":[{"name":"shared_preferences","dependencies":[]}],"date_created":"2020-07-07 09:25:42.259314","version":"1.20.0-3.0.pre.126"} \ No newline at end of file diff --git a/lib/src/tab_controller.dart b/lib/src/tab_controller.dart index a443b86b..6952b504 100644 --- a/lib/src/tab_controller.dart +++ b/lib/src/tab_controller.dart @@ -5,7 +5,7 @@ part of 'hooks.dart'; /// See also: /// - [TabController] TabController useTabController({ - @required int length, + @required int initialLength, TickerProvider vsync, int initialIndex = 0, List keys, @@ -14,7 +14,7 @@ TabController useTabController({ return use(_TabControllerHook( vsync: vsync, - length: length, + length: initialLength, initialIndex: initialIndex, keys: keys, )); diff --git a/test/use_tab_controller_test.dart b/test/use_tab_controller_test.dart index d85f38f9..204d1197 100644 --- a/test/use_tab_controller_test.dart +++ b/test/use_tab_controller_test.dart @@ -1,49 +1,104 @@ import 'package:flutter/material.dart'; +import 'package:flutter/scheduler.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_hooks/src/framework.dart'; import 'package:flutter_hooks/src/hooks.dart'; +import 'mock.dart'; + void main() { group('useTabController', () { testWidgets("returns a TabController that doesn't change", (tester) async { - final rebuilder = ValueNotifier(0); TabController controller; + TabController controller2; - await tester.pumpWidget(HookBuilder( - builder: (context) { - controller = useTabController(length: 1); + await tester.pumpWidget( + HookBuilder(builder: (context) { + controller = useTabController(initialLength: 1); + return Container(); + }), + ); - useValueNotifier(rebuilder); + expect(controller, isA()); - return const SizedBox(); - }, - )); + await tester.pumpWidget( + HookBuilder(builder: (context) { + controller2 = useTabController(initialLength: 1); + return Container(); + }), + ); - expect(controller, isA()); + expect(identical(controller, controller2), isTrue); + }); + testWidgets('changing length is no-op', (tester) async { + TabController controller; + + await tester.pumpWidget( + HookBuilder(builder: (context) { + controller = useTabController(initialLength: 1); + return Container(); + }), + ); + + expect(controller.length, 1); - final oldController = controller; - rebuilder.notifyListeners(); - await tester.pumpAndSettle(); + await tester.pumpWidget( + HookBuilder(builder: (context) { + controller = useTabController(initialLength: 2); + return Container(); + }), + ); - expect(identical(controller, oldController), isTrue); + expect(controller.length, 1); }); testWidgets('passes hook parameters to the TabController', (tester) async { TabController controller; - await tester.pumpWidget(HookBuilder( - builder: (context) { - controller = useTabController( - initialIndex: 2, - length: 4, - ); + await tester.pumpWidget( + HookBuilder( + builder: (context) { + controller = useTabController(initialIndex: 2, initialLength: 4); - return const SizedBox(); - }, - )); + return Container(); + }, + ), + ); expect(controller.index, 2); expect(controller.length, 4); }); + testWidgets('allows passing custom vsync', (tester) async { + final vsync = TickerProviderMock(); + final ticker = Ticker((_) {}); + when(vsync.createTicker(any)).thenReturn(ticker); + + await tester.pumpWidget( + HookBuilder( + builder: (context) { + useTabController(initialLength: 1, vsync: vsync); + + return Container(); + }, + ), + ); + + verify(vsync.createTicker(any)).called(1); + verifyNoMoreInteractions(vsync); + + await tester.pumpWidget( + HookBuilder( + builder: (context) { + useTabController(initialLength: 1, vsync: vsync); + return Container(); + }, + ), + ); + + verifyNoMoreInteractions(vsync); + ticker.dispose(); + }); }); } + +class TickerProviderMock extends Mock implements TickerProvider {} From 3fa91f9a4c693967a8ed87bc2dc0e75c60a87f47 Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Tue, 7 Jul 2020 10:02:10 +0100 Subject: [PATCH 143/384] Add useScrollController closes #41 --- README.md | 1 + lib/src/hooks.dart | 1 + lib/src/scroll_controller.dart | 58 +++++++++++++++++++++++ test/use_scroll_controller.dart | 76 +++++++++++++++++++++++++++++++ test/use_tab_controller_test.dart | 15 ++++++ 5 files changed, 151 insertions(+) create mode 100644 lib/src/scroll_controller.dart create mode 100644 test/use_scroll_controller.dart diff --git a/README.md b/README.md index e74cf763..1cd1fea3 100644 --- a/README.md +++ b/README.md @@ -342,6 +342,7 @@ A series of hooks with no particular theme. | [useTextEditingController](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useTextEditingController-constant.html) | Create a `TextEditingController` | | [useFocusNode](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useFocusNode.html) | Create a `FocusNode` | | [useTabController](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useTabController.html) | Creates and disposes a `TabController`. | +| [useScrollController](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useScrollController.html) | Creates and disposes a `ScrollController`. | ## Contributions diff --git a/lib/src/hooks.dart b/lib/src/hooks.dart index 1452b76b..efb54112 100644 --- a/lib/src/hooks.dart +++ b/lib/src/hooks.dart @@ -15,3 +15,4 @@ part 'primitives.dart'; part 'tab_controller.dart'; part 'text_controller.dart'; part 'focus.dart'; +part 'scroll_controller.dart'; diff --git a/lib/src/scroll_controller.dart b/lib/src/scroll_controller.dart new file mode 100644 index 00000000..93c12a66 --- /dev/null +++ b/lib/src/scroll_controller.dart @@ -0,0 +1,58 @@ +part of 'hooks.dart'; + +/// Creates and disposes a [ScrollController]. +/// +/// See also: +/// - [ScrollController] +ScrollController useScrollController({ + double initialScrollOffset = 0.0, + bool keepScrollOffset = true, + String debugLabel, + List keys, +}) { + return use( + _ScrollControllerHook( + initialScrollOffset: initialScrollOffset, + keepScrollOffset: keepScrollOffset, + debugLabel: debugLabel, + keys: keys, + ), + ); +} + +class _ScrollControllerHook extends Hook { + const _ScrollControllerHook({ + this.initialScrollOffset, + this.keepScrollOffset, + this.debugLabel, + List keys, + }) : super(keys: keys); + + final double initialScrollOffset; + final bool keepScrollOffset; + final String debugLabel; + + @override + HookState> createState() => + _ScrollControllerHookState(); +} + +class _ScrollControllerHookState + extends HookState { + ScrollController controller; + + @override + void initHook() { + controller = ScrollController( + initialScrollOffset: hook.initialScrollOffset, + keepScrollOffset: hook.keepScrollOffset, + debugLabel: hook.debugLabel, + ); + } + + @override + ScrollController build(BuildContext context) => controller; + + @override + void dispose() => controller.dispose(); +} diff --git a/test/use_scroll_controller.dart b/test/use_scroll_controller.dart new file mode 100644 index 00000000..1287c4b9 --- /dev/null +++ b/test/use_scroll_controller.dart @@ -0,0 +1,76 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/scheduler.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:flutter_hooks/src/framework.dart'; +import 'package:flutter_hooks/src/hooks.dart'; + +import 'mock.dart'; + +void main() { + group('useScrollController', () { + testWidgets('initial values matches with real constructor', (tester) async { + ScrollController controller; + ScrollController controller2; + + await tester.pumpWidget( + HookBuilder(builder: (context) { + controller2 = ScrollController(); + controller = useScrollController(); + return Container(); + }), + ); + + expect(controller.debugLabel, controller2.debugLabel); + expect(controller.initialScrollOffset, controller2.initialScrollOffset); + expect(controller.keepScrollOffset, controller2.keepScrollOffset); + }); + testWidgets("returns a ScrollController that doesn't change", + (tester) async { + ScrollController controller; + ScrollController controller2; + + await tester.pumpWidget( + HookBuilder(builder: (context) { + controller = useScrollController(); + return Container(); + }), + ); + + expect(controller, isA()); + + await tester.pumpWidget( + HookBuilder(builder: (context) { + controller2 = useScrollController(); + return Container(); + }), + ); + + expect(identical(controller, controller2), isTrue); + }); + + testWidgets('passes hook parameters to the ScrollController', + (tester) async { + ScrollController controller; + + await tester.pumpWidget( + HookBuilder( + builder: (context) { + controller = useScrollController( + initialScrollOffset: 42, + debugLabel: 'Hello', + keepScrollOffset: false, + ); + + return Container(); + }, + ), + ); + + expect(controller.initialScrollOffset, 42); + expect(controller.debugLabel, 'Hello'); + expect(controller.keepScrollOffset, false); + }); + }); +} + +class TickerProviderMock extends Mock implements TickerProvider {} diff --git a/test/use_tab_controller_test.dart b/test/use_tab_controller_test.dart index 204d1197..17efcdac 100644 --- a/test/use_tab_controller_test.dart +++ b/test/use_tab_controller_test.dart @@ -8,6 +8,21 @@ import 'mock.dart'; void main() { group('useTabController', () { + testWidgets('initial values matches with real constructor', (tester) async { + TabController controller; + TabController controller2; + + await tester.pumpWidget( + HookBuilder(builder: (context) { + final vsync = useSingleTickerProvider(); + controller2 = TabController(length: 4, vsync: vsync); + controller = useTabController(initialLength: 4); + return Container(); + }), + ); + + expect(controller.index, controller2.index); + }); testWidgets("returns a TabController that doesn't change", (tester) async { TabController controller; TabController controller2; From ab8580b3a48270297d833d23f252af43bafe8e27 Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Tue, 7 Jul 2020 10:05:58 +0100 Subject: [PATCH 144/384] v0.12.0 --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f1056df2..02352bee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +## 0.12.0 + +- Added `useScrollController` to create a `ScrollController` +- added `useTabController` to create a `TabController` (thanks to @Albert221) + ## 0.11.0 **Breaking change**: From 431bebb3573aeb33ffbb273437c270cac6ad072b Mon Sep 17 00:00:00 2001 From: Yusuf <33388560+zaralockheart@users.noreply.github.com> Date: Thu, 9 Jul 2020 15:14:11 +0800 Subject: [PATCH 145/384] fix wording --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 1cd1fea3..cd851174 100644 --- a/README.md +++ b/README.md @@ -155,7 +155,7 @@ Due to hooks being obtained from their index, some rules must be respected: Widget build(BuildContext context) { // starts with `use`, good name useMyHook(); - // doesn't that with `use`, could confuse people into thinking that this isn't a hook + // doesn't start with `use`, could confuse people into thinking that this isn't a hook myHook(); // .... } @@ -216,7 +216,7 @@ useC(); In this situation, `HookA` keeps its state but `HookC` gets a hard reset. This happens because when a refactoring is done, all hooks _after_ the first line impacted are disposed of. -Since `HookC` was placed after `HookB`, is got disposed of. +Since `HookC` was placed after `HookB`, it got disposed of. ## How to use From 6dcd1a89c28bf2268cbb2422a9a9616827631bec Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Mon, 13 Jul 2020 15:19:24 +0100 Subject: [PATCH 146/384] v0.12.0 --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index 99ea00c8..eceb3beb 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -3,7 +3,7 @@ description: A flutter implementation of React hooks. It adds a new kind of widg homepage: https://github.com/rrousselGit/flutter_hooks author: Remi Rousselet -version: 0.11.0 +version: 0.12.0 environment: sdk: ">=2.7.0 <3.0.0" From 2b0eb9e028a4928704a741a74bad038c972fe6a7 Mon Sep 17 00:00:00 2001 From: Lucas Eduardo Date: Sat, 18 Jul 2020 14:59:28 -0300 Subject: [PATCH 147/384] Add portuguese translation to Readme --- resources/translations/pt_br/README.md | 376 +++++++++++++++++++++++++ 1 file changed, 376 insertions(+) create mode 100644 resources/translations/pt_br/README.md diff --git a/resources/translations/pt_br/README.md b/resources/translations/pt_br/README.md new file mode 100644 index 00000000..c31b3676 --- /dev/null +++ b/resources/translations/pt_br/README.md @@ -0,0 +1,376 @@ +[English](https://github.com/rrousselGit/flutter_hooks/blob/master/README.md) | [Português](https://github.com/rrousselGit/flutter_hooks/blob/master/resources/translations/pt_br/README.md) + +[![Build Status](https://travis-ci.org/rrousselGit/flutter_hooks.svg?branch=master)](https://travis-ci.org/rrousselGit/flutter_hooks) [![codecov](https://codecov.io/gh/rrousselGit/flutter_hooks/branch/master/graph/badge.svg)](https://codecov.io/gh/rrousselGit/flutter_hooks) [![pub package](https://img.shields.io/pub/v/flutter_hooks.svg)](https://pub.dartlang.org/packages/flutter_hooks) [![pub package](https://img.shields.io/badge/Awesome-Flutter-blue.svg?longCache=true&style=flat-square)](https://github.com/Solido/awesome-flutter) + + + +# Flutter Hooks + +Uma implementação dos React Hooks para o Flutter: https://medium.com/@dan_abramov/making-sense-of-react-hooks-fdbde8803889 + +Hooks são uma nova forma de gerenciar o ciclo de vida de um `Widget`. Eles existem +por uma razão: aumentar o compartilhamento de código _entre_ widgets, removendo código +duplicado. + +## Motivação + +`StatefulWidget` sofrem de um grande problema: é bem difícil reutilizar a lógica, +por exemplo de um `initState` ou `dispose`. Um exemplo é o `AnimationController`: + +```dart +class Example extends StatefulWidget { + final Duration duration; + + const Example({Key key, @required this.duration}) + : assert(duration != null), + super(key: key); + + @override + _ExampleState createState() => _ExampleState(); +} + +class _ExampleState extends State with SingleTickerProviderStateMixin { + AnimationController _controller; + + @override + void initState() { + super.initState(); + _controller = AnimationController(vsync: this, duration: widget.duration); + } + + @override + void didUpdateWidget(Example oldWidget) { + super.didUpdateWidget(oldWidget); + if (widget.duration != oldWidget.duration) { + _controller.duration = widget.duration; + } + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Container(); + } +} +``` + +Todos os widgets que desejarem usar um `AnimationController`, terão de implementar +quase tudo isso do zero, o que é obviamente indesejável. + +Os mixins do Dart resolvem parcialmente esse problema, porém outros problemas acabam +sendo gerados: + +- Um mixin só pode ser usado uma vez por uma classe +- Mixins e classes compartilham o mesmo objeto.\ + Isso significa que se dois mixins definirem uma variável com o mesmo nome, o resultado + pode variar entre um erro de compilação ou um comportamento inesperado. + +--- + +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); + + final Duration duration; + + @override + Widget build(BuildContext context) { + final controller = useAnimationController(duration: duration); + return Container(); + } +} +``` +Esse código é equivalente ao do exemplo anterior. Ele continua descartando o +`AnimationController` no `dispose` e continua atualizando o `duration` quando +`Example.duration` muda. +Você provavelmente está pensando: + +> Para onde foi toda a lógica? + +A lógica foi movida para o `useAnimationController`, uma funcionalidade incluida +diretamente nessa biblioteca (veja [Hooks Existentes](https://github.com/rrousselGit/flutter_hooks#existing-hooks)). +Isso é o que chamamos de _Hook_. + +Hooks são novos tipos de objetos com algumas peculiaridades: + +- Eles só podem ser usados durante o método `build` de um widgets que utiliza o mix-in + `Hooks`. +- O mesmo Hook pode ser reutilizado infinitas vezes. + O código a seguir define dois `Animation Controller` independentes, e eles são + preservados quando o widget passa por um rebuild. + + ```dart + Widget build(BuildContext context) { + final controller = useAnimationController(); + final controller2 = useAnimationController(); + return Container(); + } + ``` + +- Hooks são inteiramente independentes um do outro e do widget.\ + Isso significa que eles podem ser facilmente extraídos em um pacote + e publicados no [pub](https://pub.dartlang.org/) para outros desenvolvedores + utilizarem + +## Princípios + +Similar ao `State`, hooks são armazenados no `Element` de um `Widget`. Mas, ao invés +de ter apenas um `State`, o `Element` armazena um `List`. Então, para usar um +`Hook`, deve-se chamar `Hook.use`. + +O hook retornado pelo `use` é baseado no número de vezes que ele é chamado. +A primeira chamada retorna o primeiro hook; a segunda chamada retorna o segundo hook, +a terceira retorna o terceiro hook, ... + +Se ainda não parece tão claro, veja uma simples implementação do Hook a seguir: + +```dart +class HookElement extends Element { + List _hooks; + int _hookIndex; + + T use(Hook hook) => _hooks[_hookIndex++].build(this); + + @override + performRebuild() { + _hookIndex = 0; + super.performRebuild(); + } +} +``` + +Para maiores explicações sobre como são implementados, aqui está um ótimo artigo +sobre como eles funcionam no React: https://medium.com/@ryardley/react-hooks-not-magic-just-arrays-cd4f1857236e + +## Regras + +Como os hooks são obtidos através do seu index, algumas regras devem ser respeitadas: + +### SEMPRE defina seus hooks com `use`: + +```dart +Widget build(BuildContext context) { + // começa com `use`, bom nome + useMyHook(); + // não começa com `use`, pode confundir outro desenvolvedor por achar que isso não é um hook + myHook(); + // .... +} +``` + +### CHAME os hooks sem nenhuma condição + +```dart +Widget build(BuildContext context) { + useMyHook(); + // .... +} +``` + +### NÃO envolva-os em uma condicional + +```dart +Widget build(BuildContext context) { + if (condition) { + useMyHook(); + } + // .... +} +``` + +--- + +### Sobre o hot-reload + +Como os hooks são obtidos pelo seu index, você pode achar que talvez um hot-reload durante uma refatoração pode quebrar a aplicação + +Felizmente não, `HookWidget` substitui o comportamento padrão do hot-reload para trabalhar com os hooks. Porém, existem algumas situações em que o estado de um Hook pode ser resetado. + +Considere a seguinte lista de hooks: + +```dart +useA(); +useB(0); +useC(); +``` + +Então considere que após o hot-reload, nós editamos o parâmetro do `HookB`: + +```dart +useA(); +useB(42); +useC(); +``` + +Tudo funciona perfeitamente bem; todos os hooks mantém seu estado. + +Agora considere que removemos o `HookB`. Agora temos: + +```dart +useA(); +useC(); +``` + +Nessa situação, `HookA` mantém seu estado, mas `HookC` é resetado. +Isso acontece por que quando uma refatoração é feita, todos os hooks _após_ a primeira linha são descartados. +Como `HookC` é colocado após `HookB`, ele acaba sendo descartado. + +## Como usar + +Existem duas formas de criar um hook: + +- Uma função + + Funções são a forma mais comum de se criar um Hook. Graças aos hooks serem + combináveis, uma função é capaz de combinar outros hooks para criar um hook + customizado. Como convenção, essas funções utilizam o prefixo `use`. + + O exemplo a seguir define um hook customizado que cria uma variável e mostra + seu valor no console sempre que o valor é modificado: + + ```dart + ValueNotifier useLoggedState(BuildContext context, [T initialData]) { + final result = useState(initialData); + useValueChanged(result.value, (_, __) { + print(result.value); + }); + return result; + } + ``` + +- Classe + + Quando um hook fica muito complexo, é possível converte-lo para uma classe que extende de `Hook`, que pode ser usado como `Hook.use`.\ + Como uma classe, o hook vai parecer bem similar ao `State` e terá acesso ao + ciclo de vida e métodos como `initHook`, `dispose` e `setState` + É uma boa prática esconder a classe em uma função, por exemplo: + + ```dart + Result useMyHook(BuildContext context) { + return use(const _TimeAlive()); + } + ``` + + Este exemplo defina um hook que mostra a hora quando o `State` é posto como alive. + + ```dart + class _TimeAlive extends Hook { + const _TimeAlive(); + + @override + _TimeAliveState createState() => _TimeAliveState(); + } + + class _TimeAliveState extends HookState { + DateTime start; + + @override + void initHook() { + super.initHook(); + start = DateTime.now(); + } + + @override + void build(BuildContext context) {} + + @override + void dispose() { + print(DateTime.now().difference(start)); + super.dispose(); + } + } + ``` + +## Hooks exisentes + +Flutter_hooks possui uma lista de hooks reutilizáveis. + +Eles são divididos em diferentes tipos: + +### Primitivos + +Um conjunto de hooks que interagem com diferentes ciclos de vida de um widget + +| Nome | descrição | +| ----------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------- | +| [useEffect](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useEffect.html) | Útil para side-effects e opcionalmente, cancelá-los. | +| [useState](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useState.html) | Cria uma variável e escuta suas mudanças. | +| [useMemoized](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useMemoized.html) | Guarda a instância de um objeto complexo. | +| [useContext](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useContext.html) | Obtém o `BuildContext` do `HookWidget`. | +| [useValueChanged](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useValueChanged.html) | Observa o value e chama um callback sempre que o valor muda. | + +### Object binding + +Essa categoria de hooks permite manipular objetos existentes do Flutter/Dart com hooks. +Eles serão responsáveis por criar/atualizar/descartar o objeto. + +#### dart:async: + +| nome | descrição | +| ------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------- | +| [useStream](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useStream.html) | Inscreve em uma `Stream` e retorna o estado atual num `AsyncSnapshot`. | +| [useStreamController](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useStreamController.html) | Cria um `StreamController` automaticamente descartado. | +| [useFuture](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useFuture.html) | Inscreve em uma `Future` e retorna o estado atual num `AsyncSnapshot`. | + +#### Animação: + +| nome | descrição | +| --------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------- | +| [useSingleTickerProvider](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useSingleTickerProvider.html) | Cria um único `TickerProvider`. | +| [useAnimationController](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useAnimationController.html) | Cria um `AnimationController` automaticamente descartado. | +| [useAnimation](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useAnimation.html) | Inscreve um uma `Animation` e retorna seu valor. | + +#### Listenable: + +| nome | descrição | +| ----------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------- | +| [useListenable](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useListenable.html) | Inscreve em um `Listenable` e marca o widget para um rebuild quando o listener é chamado. | +| [useValueNotifier](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useValueNotifier.html) | Cria um `ValueNotifier` automaticamente descartado. | +| [useValueListenable](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useValueListenable.html) | Inscreve em um `ValueListenable` e retorna seu valor. | + +#### Misc + +São vários hooks sem um tema particular. + +| nome | descrição | +| ----------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------ | +| [useReducer](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useReducer.html) | Uma alternativa `useState` para estados complexos. | +| [usePrevious](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/usePrevious.html) | Retorna o argumento anterior chamado [usePrevious]. | +| [useTextEditingController](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useTextEditingController-constant.html) | Cria um `TextEditingController` | +| [useFocusNode](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useFocusNode.html) | Cria um `FocusNode` | +| [useTabController](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useTabController.html) | Cria e descarta um `TabController`. | +| [useScrollController](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useScrollController.html) | Cria e descarta um `ScrollController`. | + +## Contribuições + +Contribuições são bem vindas! + +Se você acha que está faltando algum hook, sinta-se livre para abrir um pull-request. + +Para um hook customizado ser mergeado, você precisa fazer o seguinte: + +- Descrever o caso de uso + + Abrir uma issue explicando por que precisamos desse hook, como usar, ... + Isso é importante por que um hook não será mergeado se não atender um + grande número de pessoas + + Se o seu hook foi rejeitado, não se preocupe! A rejeição não significa que + ele não pode ser mergeado no future se mais pessoas se interessarem nele. + Sinta-se livre para publicar seu próprio hook como um pacote no [pub](https://pub.dev) + +- Escreva testes para o seu hook + + Um hook não será mergeado a não ser que esteja completamente testado, para evitar + quebras no futuro. + +- Adicione-o ao Readme e escreva uma documentação para ele. From 5e4ddcf33528542773a0974a1d714017ccb2259f Mon Sep 17 00:00:00 2001 From: Lucas Eduardo Date: Sat, 18 Jul 2020 15:01:15 -0300 Subject: [PATCH 148/384] Adding language options to main Readme --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index cd851174..8793ffc5 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,5 @@ +[English](https://github.com/rrousselGit/flutter_hooks/blob/master/README.md) | [Português](https://github.com/rrousselGit/flutter_hooks/blob/master/resources/translations/pt_br/README.md) + [![Build Status](https://travis-ci.org/rrousselGit/flutter_hooks.svg?branch=master)](https://travis-ci.org/rrousselGit/flutter_hooks) [![codecov](https://codecov.io/gh/rrousselGit/flutter_hooks/branch/master/graph/badge.svg)](https://codecov.io/gh/rrousselGit/flutter_hooks) [![pub package](https://img.shields.io/pub/v/flutter_hooks.svg)](https://pub.dartlang.org/packages/flutter_hooks) [![pub package](https://img.shields.io/badge/Awesome-Flutter-blue.svg?longCache=true&style=flat-square)](https://github.com/Solido/awesome-flutter) From 8de42b3f8d37dbe03ab06f15e6c11fed53199d12 Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Fri, 31 Jul 2020 11:51:34 +0100 Subject: [PATCH 149/384] fix dartdoc --- lib/src/primitives.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/src/primitives.dart b/lib/src/primitives.dart index d7ff2017..92d68d69 100644 --- a/lib/src/primitives.dart +++ b/lib/src/primitives.dart @@ -98,11 +98,11 @@ class _ValueChangedHookState } } +typedef Dispose = void Function(); + /// Useful for side-effects and optionally canceling them. /// /// [useEffect] is called synchronously on every `build`, unless -typedef Dispose = void Function(); - /// [keys] is specified. In which case [useEffect] is called again only if /// any value inside [keys] as changed. /// From a99d0e45552ebf9d4d3fdab28dcd2f3e3eb3b1f7 Mon Sep 17 00:00:00 2001 From: David Martos Date: Sun, 2 Aug 2020 00:42:20 +0200 Subject: [PATCH 150/384] useIsMounted hook --- lib/src/hooks.dart | 1 + lib/src/is_mounted.dart | 38 ++++++++++++++++++++++++++++++++++++++ test/is_mounted_test.dart | 22 ++++++++++++++++++++++ 3 files changed, 61 insertions(+) create mode 100644 lib/src/is_mounted.dart create mode 100644 test/is_mounted_test.dart diff --git a/lib/src/hooks.dart b/lib/src/hooks.dart index efb54112..1913b0c5 100644 --- a/lib/src/hooks.dart +++ b/lib/src/hooks.dart @@ -16,3 +16,4 @@ part 'tab_controller.dart'; part 'text_controller.dart'; part 'focus.dart'; part 'scroll_controller.dart'; +part 'is_mounted.dart'; diff --git a/lib/src/is_mounted.dart b/lib/src/is_mounted.dart new file mode 100644 index 00000000..282b1579 --- /dev/null +++ b/lib/src/is_mounted.dart @@ -0,0 +1,38 @@ +part of 'hooks.dart'; + +/// Returns an [IsMounted] object that you can use +/// to check if the [State] is mounted. +/// ```dart +/// final isMounted = useIsMounted(); +/// useEffect((){ +/// myFuture.then((){ +/// if (isMounted()) { +/// // Do something +/// } +/// }); +/// return null; +/// }, []); +/// ``` +/// See also: +/// * The [State.mounted] property. +IsMounted useIsMounted() { + final isMounted = IsMounted(); + useEffect(() { + return () { + isMounted._mounted = false; + }; + }, const []); + return isMounted; +} + +/// Mutable class that holds the current mounted value. +/// See also: +/// * The [State.mounted] property +class IsMounted { + bool _mounted = true; + + /// Returns whether or not the state is mounted. + bool call() { + return _mounted; + } +} diff --git a/test/is_mounted_test.dart b/test/is_mounted_test.dart new file mode 100644 index 00000000..c30828f0 --- /dev/null +++ b/test/is_mounted_test.dart @@ -0,0 +1,22 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + testWidgets('useIsMounted', (tester) async { + IsMounted isMounted; + + await tester.pumpWidget(HookBuilder( + builder: (context) { + isMounted = useIsMounted(); + return Container(); + }, + )); + + expect(isMounted(), true); + + await tester.pumpWidget(Container()); + + expect(isMounted(), false); + }); +} From 9d993f9877d68c2dd42496336dee702bc3b98513 Mon Sep 17 00:00:00 2001 From: David Martos Date: Sun, 2 Aug 2020 23:18:08 +0200 Subject: [PATCH 151/384] Code review changes --- lib/src/hooks.dart | 1 - lib/src/is_mounted.dart | 38 ----------------------------- lib/src/misc.dart | 54 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 54 insertions(+), 39 deletions(-) delete mode 100644 lib/src/is_mounted.dart diff --git a/lib/src/hooks.dart b/lib/src/hooks.dart index 1913b0c5..efb54112 100644 --- a/lib/src/hooks.dart +++ b/lib/src/hooks.dart @@ -16,4 +16,3 @@ part 'tab_controller.dart'; part 'text_controller.dart'; part 'focus.dart'; part 'scroll_controller.dart'; -part 'is_mounted.dart'; diff --git a/lib/src/is_mounted.dart b/lib/src/is_mounted.dart deleted file mode 100644 index 282b1579..00000000 --- a/lib/src/is_mounted.dart +++ /dev/null @@ -1,38 +0,0 @@ -part of 'hooks.dart'; - -/// Returns an [IsMounted] object that you can use -/// to check if the [State] is mounted. -/// ```dart -/// final isMounted = useIsMounted(); -/// useEffect((){ -/// myFuture.then((){ -/// if (isMounted()) { -/// // Do something -/// } -/// }); -/// return null; -/// }, []); -/// ``` -/// See also: -/// * The [State.mounted] property. -IsMounted useIsMounted() { - final isMounted = IsMounted(); - useEffect(() { - return () { - isMounted._mounted = false; - }; - }, const []); - return isMounted; -} - -/// Mutable class that holds the current mounted value. -/// See also: -/// * The [State.mounted] property -class IsMounted { - bool _mounted = true; - - /// Returns whether or not the state is mounted. - bool call() { - return _mounted; - } -} diff --git a/lib/src/misc.dart b/lib/src/misc.dart index 24fce447..7bb391de 100644 --- a/lib/src/misc.dart +++ b/lib/src/misc.dart @@ -143,3 +143,57 @@ class _ReassembleHookState extends HookState { @override void build(BuildContext context) {} } + +/// Returns an [IsMounted] object that you can use +/// to check if the [State] is mounted. +/// +/// ```dart +/// final isMounted = useIsMounted(); +/// useEffect((){ +/// myFuture.then((){ +/// if (isMounted()) { +/// // Do something +/// } +/// }); +/// return null; +/// }, []); +/// ``` +/// +/// See also: +/// * The [State.mounted] property. +IsMounted useIsMounted() { + return use(const _IsMountedHook()); +} + +class _IsMountedHook extends Hook { + const _IsMountedHook(); + + @override + _IsMountedHookState createState() => _IsMountedHookState(); +} + +class _IsMountedHookState extends HookState { + + final _isMounted = IsMounted(); + + @override + IsMounted build(BuildContext context) => _isMounted; + + @override + void dispose() { + _isMounted._mounted = false; + super.dispose(); + } +} + +/// Mutable class that holds the current mounted value. +/// See also: +/// * The [State.mounted] property +class IsMounted { + bool _mounted = true; + + /// Returns whether or not the state is mounted. + bool call() { + return _mounted; + } +} From 98afb74261c1e98b0c9c488b0a21dba8237e7fed Mon Sep 17 00:00:00 2001 From: David Martos Date: Sun, 2 Aug 2020 23:24:06 +0200 Subject: [PATCH 152/384] formatting --- lib/src/misc.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/src/misc.dart b/lib/src/misc.dart index 7bb391de..2e189d63 100644 --- a/lib/src/misc.dart +++ b/lib/src/misc.dart @@ -173,7 +173,6 @@ class _IsMountedHook extends Hook { } class _IsMountedHookState extends HookState { - final _isMounted = IsMounted(); @override From 5bfbc84287e3d98e3497d834784ac976e614addf Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Mon, 3 Aug 2020 10:26:28 +0100 Subject: [PATCH 153/384] 0.13.0 --- CHANGELOG.md | 4 ++++ README.md | 1 + pubspec.yaml | 2 +- 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 02352bee..12182ad0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.13.0 + +- Added `useIsMounted` to determine whether a widget was destroyed or not (thanks to @davidmartos96) + ## 0.12.0 - Added `useScrollController` to create a `ScrollController` diff --git a/README.md b/README.md index 8793ffc5..d25763bb 100644 --- a/README.md +++ b/README.md @@ -345,6 +345,7 @@ A series of hooks with no particular theme. | [useFocusNode](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useFocusNode.html) | Create a `FocusNode` | | [useTabController](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useTabController.html) | Creates and disposes a `TabController`. | | [useScrollController](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useScrollController.html) | Creates and disposes a `ScrollController`. | +| [useIsMounted](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useIsMounted.html) | An equivalent to `State.mounted` for hooks | ## Contributions diff --git a/pubspec.yaml b/pubspec.yaml index eceb3beb..be90b273 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -3,7 +3,7 @@ description: A flutter implementation of React hooks. It adds a new kind of widg homepage: https://github.com/rrousselGit/flutter_hooks author: Remi Rousselet -version: 0.12.0 +version: 0.13.0 environment: sdk: ">=2.7.0 <3.0.0" From 053cfb0cf24e4302d0dbd33eb369571dfa293e9a Mon Sep 17 00:00:00 2001 From: Afsar Pasha <25363752+Afsar-Pasha@users.noreply.github.com> Date: Sat, 8 Aug 2020 14:55:39 +0000 Subject: [PATCH 154/384] Removed unnecessary check in Hook.shouldPreserveState --- lib/src/framework.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/framework.dart b/lib/src/framework.dart index 71ee4ec0..b7cb2d18 100644 --- a/lib/src/framework.dart +++ b/lib/src/framework.dart @@ -170,7 +170,7 @@ Calling them outside of build method leads to an unstable state and is therefore return true; } // is one list is null and the other one isn't, or if they have different size - if ((p1 != p2 && (p1 == null || p2 == null)) || p1.length != p2.length) { + if (p1 == null || p2 == null || p1.length != p2.length) { return false; } From 461b070aefa3a33904d58fcf0792fbe11a14cb8a Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Wed, 12 Aug 2020 09:04:03 +0100 Subject: [PATCH 155/384] Use a function instead of class for useIsMounted --- example/.flutter-plugins-dependencies | 2 +- lib/src/misc.dart | 18 +++++------------- 2 files changed, 6 insertions(+), 14 deletions(-) diff --git a/example/.flutter-plugins-dependencies b/example/.flutter-plugins-dependencies index 32b57698..76ab6c02 100644 --- a/example/.flutter-plugins-dependencies +++ b/example/.flutter-plugins-dependencies @@ -1 +1 @@ -{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"shared_preferences","path":"/Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/shared_preferences-0.4.3/","dependencies":[]}],"android":[{"name":"shared_preferences","path":"/Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/shared_preferences-0.4.3/","dependencies":[]}],"macos":[],"linux":[],"windows":[],"web":[]},"dependencyGraph":[{"name":"shared_preferences","dependencies":[]}],"date_created":"2020-07-07 09:25:42.259314","version":"1.20.0-3.0.pre.126"} \ No newline at end of file +{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"shared_preferences","path":"/Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/shared_preferences-0.4.3/","dependencies":[]}],"android":[{"name":"shared_preferences","path":"/Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/shared_preferences-0.4.3/","dependencies":[]}],"macos":[],"linux":[],"windows":[],"web":[]},"dependencyGraph":[{"name":"shared_preferences","dependencies":[]}],"date_created":"2020-08-12 09:03:32.500284","version":"1.21.0-8.0.pre.130"} \ No newline at end of file diff --git a/lib/src/misc.dart b/lib/src/misc.dart index 2e189d63..bc803202 100644 --- a/lib/src/misc.dart +++ b/lib/src/misc.dart @@ -173,26 +173,18 @@ class _IsMountedHook extends Hook { } class _IsMountedHookState extends HookState { - final _isMounted = IsMounted(); + bool _mounted = true; @override IsMounted build(BuildContext context) => _isMounted; + bool _isMounted() => _mounted; + @override void dispose() { - _isMounted._mounted = false; + _mounted = false; super.dispose(); } } -/// Mutable class that holds the current mounted value. -/// See also: -/// * The [State.mounted] property -class IsMounted { - bool _mounted = true; - - /// Returns whether or not the state is mounted. - bool call() { - return _mounted; - } -} +typedef IsMounted = bool Function(); From 75146eedd2ae0b3d6487945469d40d0e73224962 Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Wed, 12 Aug 2020 09:04:48 +0100 Subject: [PATCH 156/384] 0.13.1 --- CHANGELOG.md | 4 ++++ pubspec.yaml | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 12182ad0..764114bb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.13.1 + +- `useIsMounted` now returns a function instead of a callable class. + ## 0.13.0 - Added `useIsMounted` to determine whether a widget was destroyed or not (thanks to @davidmartos96) diff --git a/pubspec.yaml b/pubspec.yaml index be90b273..cfe72a25 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -3,7 +3,7 @@ description: A flutter implementation of React hooks. It adds a new kind of widg homepage: https://github.com/rrousselGit/flutter_hooks author: Remi Rousselet -version: 0.13.0 +version: 0.13.1 environment: sdk: ">=2.7.0 <3.0.0" From 9618c9aba1b4464cc310889be63b54fcb94f2b69 Mon Sep 17 00:00:00 2001 From: Ionut-Cristian Florescu Date: Fri, 21 Aug 2020 12:30:51 +0300 Subject: [PATCH 157/384] Fix typo in English README Should be *always **prefix** your hooks with `use`* instead of *always prefer your hooks with `use`*. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d25763bb..39426f7c 100644 --- a/README.md +++ b/README.md @@ -151,7 +151,7 @@ how they did it in React: https://medium.com/@ryardley/react-hooks-not-magic-jus Due to hooks being obtained from their index, some rules must be respected: -### DO always prefer your hooks with `use`: +### DO always prefix your hooks with `use`: ```dart Widget build(BuildContext context) { From d0006c442f51cb32f49d3946b5d746022f7b5a82 Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Sat, 22 Aug 2020 15:26:15 +0100 Subject: [PATCH 158/384] Fix missing rebuild on hot-reload --- CHANGELOG.md | 4 +++ analysis_options.yaml | 2 ++ example/.flutter-plugins-dependencies | 2 +- lib/src/framework.dart | 23 +++++++------- pubspec.yaml | 4 +-- test/hook_widget_test.dart | 43 +++++++++++++++++++++++++++ 6 files changed, 63 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 764114bb..417d53a6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.13.2 + +- Fixed a bug where on hot-reload, a `HookWidget` could potentailly not rebuild + ## 0.13.1 - `useIsMounted` now returns a function instead of a callable class. diff --git a/analysis_options.yaml b/analysis_options.yaml index dc5b2590..263d685a 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -62,3 +62,5 @@ linter: # Conflicts with disabling `implicit-dynamic` avoid_annotating_with_dynamic: false + # Too verbose + diagnostic_describe_all_properties: false \ No newline at end of file diff --git a/example/.flutter-plugins-dependencies b/example/.flutter-plugins-dependencies index 76ab6c02..b87c0343 100644 --- a/example/.flutter-plugins-dependencies +++ b/example/.flutter-plugins-dependencies @@ -1 +1 @@ -{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"shared_preferences","path":"/Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/shared_preferences-0.4.3/","dependencies":[]}],"android":[{"name":"shared_preferences","path":"/Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/shared_preferences-0.4.3/","dependencies":[]}],"macos":[],"linux":[],"windows":[],"web":[]},"dependencyGraph":[{"name":"shared_preferences","dependencies":[]}],"date_created":"2020-08-12 09:03:32.500284","version":"1.21.0-8.0.pre.130"} \ No newline at end of file +{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"shared_preferences","path":"/Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/shared_preferences-0.4.3/","dependencies":[]}],"android":[{"name":"shared_preferences","path":"/Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/shared_preferences-0.4.3/","dependencies":[]}],"macos":[],"linux":[],"windows":[],"web":[]},"dependencyGraph":[{"name":"shared_preferences","dependencies":[]}],"date_created":"2020-08-22 14:58:12.080328","version":"1.21.0-10.0.pre.105"} \ No newline at end of file diff --git a/lib/src/framework.dart b/lib/src/framework.dart index b7cb2d18..b9fbb7e0 100644 --- a/lib/src/framework.dart +++ b/lib/src/framework.dart @@ -367,6 +367,18 @@ mixin HookElement on ComponentElement { super.didChangeDependencies(); } + @override + void reassemble() { + super.reassemble(); + _isOptionalRebuild = false; + _debugDidReassemble = true; + if (_hooks != null) { + for (final hook in _hooks) { + hook.value.reassemble(); + } + } + } + @override Widget build() { // Check whether we can cancel the rebuild (caused by HookState.mayNeedRebuild). @@ -494,17 +506,6 @@ Type mismatch between hooks: } super.deactivate(); } - - @override - void reassemble() { - super.reassemble(); - _debugDidReassemble = true; - if (_hooks != null) { - for (final hook in _hooks) { - hook.value.reassemble(); - } - } - } } /// A [Widget] that can use [Hook] diff --git a/pubspec.yaml b/pubspec.yaml index cfe72a25..6b127061 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,9 +1,7 @@ name: flutter_hooks description: A flutter implementation of React hooks. It adds a new kind of widget with enhanced code reuse. homepage: https://github.com/rrousselGit/flutter_hooks -author: Remi Rousselet - -version: 0.13.1 +version: 0.13.2 environment: sdk: ">=2.7.0 <3.0.0" diff --git a/test/hook_widget_test.dart b/test/hook_widget_test.dart index 2ef9912f..c844d33f 100644 --- a/test/hook_widget_test.dart +++ b/test/hook_widget_test.dart @@ -1216,6 +1216,49 @@ void main() { hotReload(tester); await tester.pump(); }); + + testWidgets('refreshes identical widgets on hot-reload', (tester) async { + var value = 0; + final child = HookBuilder(builder: (context) { + use(MayHaveChangedOnReassemble()); + + return Text('$value', textDirection: TextDirection.ltr); + }); + + await tester.pumpWidget(child); + + expect(find.text('0'), findsOneWidget); + + value = 1; + + // ignore: unawaited_futures + tester.binding.reassembleApplication(); + await tester.pump(); + + expect(find.text('1'), findsOneWidget); + }); +} + +class MayHaveChangedOnReassemble extends Hook { + @override + MayHaveChangedOnReassembleState createState() => + MayHaveChangedOnReassembleState(); +} + +class MayHaveChangedOnReassembleState + extends HookState { + @override + void reassemble() { + markMayNeedRebuild(); + } + + @override + bool shouldRebuild() { + return false; + } + + @override + void build(BuildContext context) {} } class MyHook extends Hook { From 739c2a85275132fb78bcdc62f98220c949a28580 Mon Sep 17 00:00:00 2001 From: Frank Moreno Date: Mon, 24 Aug 2020 19:04:12 -0500 Subject: [PATCH 159/384] refactor: _TextEditingControllerHook with multiple constructors --- lib/src/text_controller.dart | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/lib/src/text_controller.dart b/lib/src/text_controller.dart index 6bbd7dd9..67081311 100644 --- a/lib/src/text_controller.dart +++ b/lib/src/text_controller.dart @@ -8,13 +8,13 @@ class _TextEditingControllerHookCreator { /// The [text] parameter can be used to set the initial value of the /// controller. TextEditingController call({String text, List keys}) { - return use(_TextEditingControllerHook(text, null, keys)); + return use(_TextEditingControllerHook(text, keys)); } /// Creates a [TextEditingController] from the initial [value] that will /// be disposed automatically. TextEditingController fromValue(TextEditingValue value, [List keys]) { - return use(_TextEditingControllerHook(null, value, keys)); + return use(_TextEditingControllerHook.fromValue(value, keys)); } } @@ -55,14 +55,16 @@ const useTextEditingController = _TextEditingControllerHookCreator(); class _TextEditingControllerHook extends Hook { const _TextEditingControllerHook( - this.initialText, + this.initialText, [ + List keys, + ]) : initialValue = null, + super(keys: keys); + + const _TextEditingControllerHook.fromValue( this.initialValue, [ List keys, - ]) : assert( - initialText == null || initialValue == null, - "initialText and intialValue can't both be set on a call to " - 'useTextEditingController!', - ), + ]) : initialText = null, + assert(initialValue != null, "initialValue can't be null"), super(keys: keys); final String initialText; From c6f759b5fe7197918f94f5d3fa62011f7ad4bd54 Mon Sep 17 00:00:00 2001 From: Frank Moreno Date: Mon, 24 Aug 2020 19:05:39 -0500 Subject: [PATCH 160/384] feat: adding text for useTextEditingController.value(null) --- test/use_text_editing_controller_test.dart | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/test/use_text_editing_controller_test.dart b/test/use_text_editing_controller_test.dart index 32833fa8..0464140e 100644 --- a/test/use_text_editing_controller_test.dart +++ b/test/use_text_editing_controller_test.dart @@ -62,6 +62,20 @@ void main() { expect(controller.text, initialText); }); + testWidgets('useTextEditingController throws error on null value', + (tester) async { + await tester.pumpWidget(HookBuilder( + builder: (context) { + try { + useTextEditingController.fromValue(null); + } catch (e) { + expect(e, isAssertionError); + } + return Container(); + }, + )); + }); + testWidgets('respects initial value property', (tester) async { final rebuilder = ValueNotifier(0); const initialValue = TextEditingValue( From d611ca575592c86370d30f50c38bd5fd0b3186e9 Mon Sep 17 00:00:00 2001 From: Frank Moreno Date: Mon, 24 Aug 2020 19:07:33 -0500 Subject: [PATCH 161/384] refactor: _TextEditingControllerHookCreator with no const constructor for 100% of coverage --- lib/src/text_controller.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/src/text_controller.dart b/lib/src/text_controller.dart index 67081311..109a1970 100644 --- a/lib/src/text_controller.dart +++ b/lib/src/text_controller.dart @@ -1,7 +1,7 @@ part of 'hooks.dart'; class _TextEditingControllerHookCreator { - const _TextEditingControllerHookCreator(); + _TextEditingControllerHookCreator(); /// Creates a [TextEditingController] that will be disposed automatically. /// @@ -51,7 +51,7 @@ class _TextEditingControllerHookCreator { /// /// See also: /// - [TextEditingController], which this hook creates. -const useTextEditingController = _TextEditingControllerHookCreator(); +final useTextEditingController = _TextEditingControllerHookCreator(); class _TextEditingControllerHook extends Hook { const _TextEditingControllerHook( From f3414f91f18ed37745e41d6dc7a879161db17cf9 Mon Sep 17 00:00:00 2001 From: Frank Moreno Date: Mon, 24 Aug 2020 19:50:16 -0500 Subject: [PATCH 162/384] generalizing useFocusNode with input parameters --- lib/src/focus.dart | 42 +++++++++++++++++++++++++++++++++++++++--- 1 file changed, 39 insertions(+), 3 deletions(-) diff --git a/lib/src/focus.dart b/lib/src/focus.dart index b3dbcbaa..e58c5012 100644 --- a/lib/src/focus.dart +++ b/lib/src/focus.dart @@ -4,10 +4,35 @@ part of 'hooks.dart'; /// /// See also: /// - [FocusNode] -FocusNode useFocusNode() => use(const _FocusNodeHook()); +FocusNode useFocusNode({ + String debugLabel, + FocusOnKeyCallback onKey, + bool skipTraversal = false, + bool canRequestFocus = true, + bool descendantsAreFocusable = true, +}) => + use(_FocusNodeHook( + debugLabel: debugLabel, + onKey: onKey, + skipTraversal: skipTraversal, + canRequestFocus: canRequestFocus, + descendantsAreFocusable: descendantsAreFocusable, + )); class _FocusNodeHook extends Hook { - const _FocusNodeHook(); + const _FocusNodeHook({ + this.debugLabel, + this.onKey, + this.skipTraversal, + this.canRequestFocus, + this.descendantsAreFocusable, + }); + + final String debugLabel; + final FocusOnKeyCallback onKey; + final bool skipTraversal; + final bool canRequestFocus; + final bool descendantsAreFocusable; @override _FocusNodeHookState createState() { @@ -16,7 +41,18 @@ class _FocusNodeHook extends Hook { } class _FocusNodeHookState extends HookState { - final _focusNode = FocusNode(); + FocusNode _focusNode; + + @override + void initHook() { + _focusNode = FocusNode( + debugLabel: hook.debugLabel, + onKey: hook.onKey, + skipTraversal: hook.skipTraversal, + canRequestFocus: hook.canRequestFocus, + descendantsAreFocusable: hook.descendantsAreFocusable, + ); + } @override FocusNode build(BuildContext context) => _focusNode; From b7ef993a15bda6f0c5b2b060c792b160a0b69cf7 Mon Sep 17 00:00:00 2001 From: Frank Moreno Date: Mon, 24 Aug 2020 20:56:21 -0500 Subject: [PATCH 163/384] adding '_test' to use_scroll_controller_test.dart --- ...use_scroll_controller.dart => use_scroll_controller_test.dart} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename test/{use_scroll_controller.dart => use_scroll_controller_test.dart} (100%) diff --git a/test/use_scroll_controller.dart b/test/use_scroll_controller_test.dart similarity index 100% rename from test/use_scroll_controller.dart rename to test/use_scroll_controller_test.dart From 2ea4f01d97dd1e96f308ac064ce44b32e52b7b71 Mon Sep 17 00:00:00 2001 From: Frank Moreno Date: Tue, 25 Aug 2020 13:05:23 -0500 Subject: [PATCH 164/384] HookWidget.debugFillProperties using HookState.diagnosticsProperty --- lib/src/framework.dart | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/lib/src/framework.dart b/lib/src/framework.dart index b9fbb7e0..8c453c73 100644 --- a/lib/src/framework.dart +++ b/lib/src/framework.dart @@ -281,6 +281,12 @@ abstract class HookState> { .._isOptionalRebuild = false ..markNeedsBuild(); } + + /// The [diagnosticsProperty] is the default propertity to add to `properties` + /// param of the [Element.debugFillProperties]. + /// If it's null, it will be skipped + @visibleForTesting + DiagnosticsProperty get diagnosticsProperty => null; } class _Entry extends LinkedListEntry<_Entry> { @@ -506,6 +512,19 @@ Type mismatch between hooks: } super.deactivate(); } + + /// By default read every [HookState.diagnosticsProperty], and if it's not + /// `null`, will be added to [properties] + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + for (final hookState in debugHooks) { + final property = hookState.diagnosticsProperty; + if (property != null) { + properties.add(property); + } + } + } } /// A [Widget] that can use [Hook] From 3f7b385f8a65bfe6839c89588c2c9e51c5c88629 Mon Sep 17 00:00:00 2001 From: Frank Moreno Date: Tue, 25 Aug 2020 13:36:09 -0500 Subject: [PATCH 165/384] Refactor HookState.diagnosticsProperty to HookState.debugFillProperties --- lib/src/framework.dart | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/lib/src/framework.dart b/lib/src/framework.dart index 8c453c73..e652e5c8 100644 --- a/lib/src/framework.dart +++ b/lib/src/framework.dart @@ -282,11 +282,9 @@ abstract class HookState> { ..markNeedsBuild(); } - /// The [diagnosticsProperty] is the default propertity to add to `properties` - /// param of the [Element.debugFillProperties]. - /// If it's null, it will be skipped - @visibleForTesting - DiagnosticsProperty get diagnosticsProperty => null; + /// Equivalent of [Element.debugFillProperties] for [HookState] + @mustCallSuper + void debugFillProperties(DiagnosticPropertiesBuilder properties) {} } class _Entry extends LinkedListEntry<_Entry> { @@ -513,16 +511,12 @@ Type mismatch between hooks: super.deactivate(); } - /// By default read every [HookState.diagnosticsProperty], and if it's not - /// `null`, will be added to [properties] + /// Add properties [properties] using every [HookState.debugFillProperties] @override void debugFillProperties(DiagnosticPropertiesBuilder properties) { super.debugFillProperties(properties); for (final hookState in debugHooks) { - final property = hookState.diagnosticsProperty; - if (property != null) { - properties.add(property); - } + hookState.debugFillProperties(properties); } } } From a5b02d1e78c2150ff40f8dffd255708e828c1547 Mon Sep 17 00:00:00 2001 From: Frank Moreno Date: Tue, 25 Aug 2020 13:40:23 -0500 Subject: [PATCH 166/384] HookState now implements Diagnosticable --- lib/src/framework.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/src/framework.dart b/lib/src/framework.dart index e652e5c8..b94b5c83 100644 --- a/lib/src/framework.dart +++ b/lib/src/framework.dart @@ -203,7 +203,7 @@ Calling them outside of build method leads to an unstable state and is therefore } /// The logic and internal state for a [HookWidget] -abstract class HookState> { +abstract class HookState> implements Diagnosticable { /// Equivalent of [State.context] for [HookState] @protected BuildContext get context => _element; @@ -283,6 +283,7 @@ abstract class HookState> { } /// Equivalent of [Element.debugFillProperties] for [HookState] + @override @mustCallSuper void debugFillProperties(DiagnosticPropertiesBuilder properties) {} } From 0c77ffae52c9f5a7f717143a4f69a5a8b1a7f085 Mon Sep 17 00:00:00 2001 From: Frank Moreno Date: Tue, 25 Aug 2020 13:43:54 -0500 Subject: [PATCH 167/384] Fix: HookState doesn't implement Diagnosticable, but also use it as mixin --- lib/src/framework.dart | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/lib/src/framework.dart b/lib/src/framework.dart index b94b5c83..f086a24d 100644 --- a/lib/src/framework.dart +++ b/lib/src/framework.dart @@ -203,7 +203,7 @@ Calling them outside of build method leads to an unstable state and is therefore } /// The logic and internal state for a [HookWidget] -abstract class HookState> implements Diagnosticable { +abstract class HookState> with Diagnosticable { /// Equivalent of [State.context] for [HookState] @protected BuildContext get context => _element; @@ -281,11 +281,6 @@ abstract class HookState> implements Diagnosticable { .._isOptionalRebuild = false ..markNeedsBuild(); } - - /// Equivalent of [Element.debugFillProperties] for [HookState] - @override - @mustCallSuper - void debugFillProperties(DiagnosticPropertiesBuilder properties) {} } class _Entry extends LinkedListEntry<_Entry> { From 1064c351352db6381c6f25587f044c76fdbc4996 Mon Sep 17 00:00:00 2001 From: Simon B Date: Thu, 27 Aug 2020 13:10:31 +0200 Subject: [PATCH 168/384] Create README.md Add minimal explanation of the folder and what the .g.dart files are about. --- example/lib/star_wars/README.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 example/lib/star_wars/README.md diff --git a/example/lib/star_wars/README.md b/example/lib/star_wars/README.md new file mode 100644 index 00000000..efa910f7 --- /dev/null +++ b/example/lib/star_wars/README.md @@ -0,0 +1,5 @@ +# Star wars API + +This folder is specifically for handling the Star wars API. This is one API endpoint: https://swapi.co/api/planets + +To understand the generated .g.dart code see https://flutter.dev/docs/development/data-and-backend/json#serializing-json-using-code-generation-libraries From d788c14d435b38eea45ab05846e1680fa2ad630b Mon Sep 17 00:00:00 2001 From: Frank Moreno Date: Sun, 30 Aug 2020 17:33:46 -0500 Subject: [PATCH 169/384] debugFillProperties for memoized hook --- lib/src/primitives.dart | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/lib/src/primitives.dart b/lib/src/primitives.dart index 92d68d69..ef43f46f 100644 --- a/lib/src/primitives.dart +++ b/lib/src/primitives.dart @@ -41,6 +41,12 @@ class _MemoizedHookState extends HookState> { T build(BuildContext context) { return value; } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(DiagnosticsProperty('useMemoized<$T>', value)); + } } /// Watches a value and calls a callback whenever the value changed. From 485787f29580d13afa74b4a96bfb9aed3010e84b Mon Sep 17 00:00:00 2001 From: Frank Moreno Date: Sun, 30 Aug 2020 17:34:05 -0500 Subject: [PATCH 170/384] debugFillProperties for state hook --- lib/src/primitives.dart | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/lib/src/primitives.dart b/lib/src/primitives.dart index ef43f46f..7dd7c12c 100644 --- a/lib/src/primitives.dart +++ b/lib/src/primitives.dart @@ -247,4 +247,10 @@ class _StateHookState extends HookState, _StateHook> { void _listener() { setState(() {}); } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(DiagnosticsProperty('useState<$T>', _state.value)); + } } From 7c7a200ce81f871f8dcbf43f3da5cfc48b8a9ff7 Mon Sep 17 00:00:00 2001 From: Frank Moreno Date: Mon, 31 Aug 2020 00:05:31 -0500 Subject: [PATCH 171/384] useState appears in debugFillProperties --- test/use_state_test.dart | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/test/use_state_test.dart b/test/use_state_test.dart index 226285be..684804fc 100644 --- a/test/use_state_test.dart +++ b/test/use_state_test.dart @@ -1,3 +1,5 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; @@ -71,4 +73,30 @@ void main() { // ignore: invalid_use_of_protected_member expect(() => state.hasListeners, throwsFlutterError); }); + + testWidgets('debugFillProperties should print state hook ', (tester) async { + ValueNotifier state; + HookElement element; + final hookWidget = HookBuilder( + builder: (context) { + element = context as HookElement; + state = useState(0); + return const SizedBox(); + }, + ); + await tester.pumpWidget(hookWidget); + + expect(state.value, 0); + + expect( + element + .toDiagnosticsNode(style: DiagnosticsTreeStyle.offstage) + .toStringDeep(), + equalsIgnoringHashCodes( + 'HookBuilder\n' + ' │ useState: 0\n' + ' └SizedBox(renderObject: RenderConstrainedBox#00000)\n', + ), + ); + }); } From ba08b8631d2b4f5b2077be53550522ed31858e10 Mon Sep 17 00:00:00 2001 From: Frank Moreno Date: Mon, 31 Aug 2020 00:06:07 -0500 Subject: [PATCH 172/384] adding linter ignore comment_references for docs --- lib/src/framework.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/src/framework.dart b/lib/src/framework.dart index f086a24d..af79a2f9 100644 --- a/lib/src/framework.dart +++ b/lib/src/framework.dart @@ -507,6 +507,7 @@ Type mismatch between hooks: super.deactivate(); } + // ignore: comment_references /// Add properties [properties] using every [HookState.debugFillProperties] @override void debugFillProperties(DiagnosticPropertiesBuilder properties) { From 769341ccff1c9c3a988ccd880d6d3b274e68f935 Mon Sep 17 00:00:00 2001 From: Frank Moreno Date: Mon, 31 Aug 2020 00:08:36 -0500 Subject: [PATCH 173/384] testing state hook change --- test/use_state_test.dart | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/test/use_state_test.dart b/test/use_state_test.dart index 684804fc..0e2aae1f 100644 --- a/test/use_state_test.dart +++ b/test/use_state_test.dart @@ -86,8 +86,6 @@ void main() { ); await tester.pumpWidget(hookWidget); - expect(state.value, 0); - expect( element .toDiagnosticsNode(style: DiagnosticsTreeStyle.offstage) @@ -98,5 +96,20 @@ void main() { ' └SizedBox(renderObject: RenderConstrainedBox#00000)\n', ), ); + + state.value++; + + await tester.pump(); + + expect( + element + .toDiagnosticsNode(style: DiagnosticsTreeStyle.offstage) + .toStringDeep(), + equalsIgnoringHashCodes( + 'HookBuilder\n' + ' │ useState: 1\n' + ' └SizedBox(renderObject: RenderConstrainedBox#00000)\n', + ), + ); }); } From 1dfdd615c75535068e36d3fda7d3540e3465bbf2 Mon Sep 17 00:00:00 2001 From: Frank Moreno Date: Mon, 31 Aug 2020 00:59:08 -0500 Subject: [PATCH 174/384] debugFillProperties test for useMemoized --- test/memoized_test.dart | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/test/memoized_test.dart b/test/memoized_test.dart index 8fd420a1..efc0e46a 100644 --- a/test/memoized_test.dart +++ b/test/memoized_test.dart @@ -1,4 +1,7 @@ +import 'dart:math'; + import 'package:flutter/widgets.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'mock.dart'; @@ -280,6 +283,33 @@ void main() { verifyNoMoreInteractions(valueBuilder); }); + + testWidgets('debugFillProperties should print memoized hook ', + (tester) async { + HookElement element; + + final hookWidget = HookBuilder( + builder: (context) { + element = context as HookElement; + useMemoized>(() => Future.value(10)); + useMemoized(() => 43); + return const SizedBox(); + }, + ); + await tester.pumpWidget(hookWidget); + + expect( + element + .toDiagnosticsNode(style: DiagnosticsTreeStyle.offstage) + .toStringDeep(), + equalsIgnoringHashCodes( + 'HookBuilder\n' + " │ useMemoized>: Instance of 'Future'\n" + ' │ useMemoized: 43\n' + ' └SizedBox(renderObject: RenderConstrainedBox#00000)\n', + ), + ); + }); } class MockValueBuilder extends Mock { From 3e22e2aadd56b38986d4578233f4b8bcf0f688ae Mon Sep 17 00:00:00 2001 From: Frank Moreno Date: Mon, 31 Aug 2020 01:22:49 -0500 Subject: [PATCH 175/384] revert useTextEditingController to const --- lib/src/text_controller.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/src/text_controller.dart b/lib/src/text_controller.dart index 109a1970..67081311 100644 --- a/lib/src/text_controller.dart +++ b/lib/src/text_controller.dart @@ -1,7 +1,7 @@ part of 'hooks.dart'; class _TextEditingControllerHookCreator { - _TextEditingControllerHookCreator(); + const _TextEditingControllerHookCreator(); /// Creates a [TextEditingController] that will be disposed automatically. /// @@ -51,7 +51,7 @@ class _TextEditingControllerHookCreator { /// /// See also: /// - [TextEditingController], which this hook creates. -final useTextEditingController = _TextEditingControllerHookCreator(); +const useTextEditingController = _TextEditingControllerHookCreator(); class _TextEditingControllerHook extends Hook { const _TextEditingControllerHook( From 9daa3e7f1dfa798f80895c461c85b8fef63c5d3c Mon Sep 17 00:00:00 2001 From: Jure Stepisnik Date: Mon, 31 Aug 2020 21:06:13 +0200 Subject: [PATCH 176/384] Added useReducer example --- example/lib/use_reducer.dart | 53 ++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 example/lib/use_reducer.dart diff --git a/example/lib/use_reducer.dart b/example/lib/use_reducer.dart new file mode 100644 index 00000000..c94db9f4 --- /dev/null +++ b/example/lib/use_reducer.dart @@ -0,0 +1,53 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; + +/// This example emulates the basic Counter app generated by the +/// `flutter create` command to demonstrates the `useReducer` hook. +/// +/// First, instead of a StatefulWidget, use a HookWidget instead! + +class State { + int counter = 0; +} + +class IncrementCounter { + IncrementCounter({this.counter}); + int counter; +} + +class UseReducerExample extends HookWidget { + @override + Widget build(BuildContext context) { + State _reducer(State state, IncrementCounter action) { + if(action is IncrementCounter) { + state.counter = state.counter + action.counter; + } + return state; + } + + // Next, invoke the `useState` function with a default value to create a + // `counter` variable that contains a `value`. Whenever the value is + // changed, this Widget will be rebuilt! + final _store = useReducer( + _reducer, + initialState: State() + ); + + return Scaffold( + appBar: AppBar( + title: const Text('useState example'), + ), + body: Center( + // Read the current value from the counter + child: Text('Button tapped ${_store.state.counter.toString()} times'), + ), + floatingActionButton: FloatingActionButton( + // When the button is pressed, update the value of the counter! This + // will trigger a rebuild, displaying the latest value in the Text + // Widget above! + onPressed: () => _store.dispatch(IncrementCounter(counter: 1)), + child: const Icon(Icons.add), + ), + ); + } +} From 10b63c34e39fcbea50f6d6cbf57d4e4bf4e8287d Mon Sep 17 00:00:00 2001 From: Jure Stepisnik Date: Mon, 31 Aug 2020 21:09:58 +0200 Subject: [PATCH 177/384] Added comments to the example --- example/lib/use_reducer.dart | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/example/lib/use_reducer.dart b/example/lib/use_reducer.dart index c94db9f4..2b94c6fe 100644 --- a/example/lib/use_reducer.dart +++ b/example/lib/use_reducer.dart @@ -6,10 +6,13 @@ import 'package:flutter_hooks/flutter_hooks.dart'; /// /// First, instead of a StatefulWidget, use a HookWidget instead! + +// Create the State class State { int counter = 0; } +// Create the actions you wish to dispatch to the reducer class IncrementCounter { IncrementCounter({this.counter}); int counter; @@ -18,6 +21,7 @@ class IncrementCounter { class UseReducerExample extends HookWidget { @override Widget build(BuildContext context) { + // Create the reducer function that will handle the actions you dispatch State _reducer(State state, IncrementCounter action) { if(action is IncrementCounter) { state.counter = state.counter + action.counter; @@ -25,8 +29,8 @@ class UseReducerExample extends HookWidget { return state; } - // Next, invoke the `useState` function with a default value to create a - // `counter` variable that contains a `value`. Whenever the value is + // Next, invoke the `useReducer` function with the reducer funtion and initial state to create a + // `_store` variable that contains the current state and dispatch. Whenever the value is // changed, this Widget will be rebuilt! final _store = useReducer( _reducer, @@ -42,7 +46,7 @@ class UseReducerExample extends HookWidget { child: Text('Button tapped ${_store.state.counter.toString()} times'), ), floatingActionButton: FloatingActionButton( - // When the button is pressed, update the value of the counter! This + // When the button is pressed, dispatch the Action you wish to trigger! This // will trigger a rebuild, displaying the latest value in the Text // Widget above! onPressed: () => _store.dispatch(IncrementCounter(counter: 1)), From 9efdb53d74b1fad132a02a5169c55bbecd4249a6 Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Sat, 5 Sep 2020 14:54:23 +0200 Subject: [PATCH 178/384] fixes https://github.com/rrousselGit/river_pod/issues/114 --- CHANGELOG.md | 4 +++ example/.flutter-plugins-dependencies | 2 +- lib/src/framework.dart | 2 ++ test/pre_build_abort_test.dart | 52 +++++++++++++++++++++++++++ 4 files changed, 59 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 417d53a6..e99ca685 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## [Unreleased] + +- Fixed a bug where a `useProvider` from riverpod potentially did not rebuild widgets + ## 0.13.2 - Fixed a bug where on hot-reload, a `HookWidget` could potentailly not rebuild diff --git a/example/.flutter-plugins-dependencies b/example/.flutter-plugins-dependencies index b87c0343..86dddfe7 100644 --- a/example/.flutter-plugins-dependencies +++ b/example/.flutter-plugins-dependencies @@ -1 +1 @@ -{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"shared_preferences","path":"/Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/shared_preferences-0.4.3/","dependencies":[]}],"android":[{"name":"shared_preferences","path":"/Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/shared_preferences-0.4.3/","dependencies":[]}],"macos":[],"linux":[],"windows":[],"web":[]},"dependencyGraph":[{"name":"shared_preferences","dependencies":[]}],"date_created":"2020-08-22 14:58:12.080328","version":"1.21.0-10.0.pre.105"} \ No newline at end of file +{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"shared_preferences","path":"/Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/shared_preferences-0.4.3/","dependencies":[]}],"android":[{"name":"shared_preferences","path":"/Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/shared_preferences-0.4.3/","dependencies":[]}],"macos":[],"linux":[],"windows":[],"web":[]},"dependencyGraph":[{"name":"shared_preferences","dependencies":[]}],"date_created":"2020-09-05 14:03:35.263785","version":"1.22.0-2.0.pre.36"} \ No newline at end of file diff --git a/lib/src/framework.dart b/lib/src/framework.dart index b9fbb7e0..cf06bef7 100644 --- a/lib/src/framework.dart +++ b/lib/src/framework.dart @@ -271,6 +271,7 @@ abstract class HookState> { .._shouldRebuildQueue.add(_Entry(shouldRebuild)) ..markNeedsBuild(); } + assert(_element.dirty, 'Bad state'); } /// Equivalent of [State.setState] for [HookState] @@ -400,6 +401,7 @@ mixin HookElement on ComponentElement { try { _buildCache = super.build(); } finally { + _isOptionalRebuild = null; _unmountAllRemainingHooks(); HookElement._currentHookElement = null; if (_needDispose != null && _needDispose.isNotEmpty) { diff --git a/test/pre_build_abort_test.dart b/test/pre_build_abort_test.dart index 55deeceb..0a78f095 100644 --- a/test/pre_build_abort_test.dart +++ b/test/pre_build_abort_test.dart @@ -6,6 +6,58 @@ import 'package:flutter_test/flutter_test.dart'; import 'mock.dart'; void main() { + testWidgets( + 'setState during build still cause mayHaveChange to rebuild the element', + (tester) async { + final number = ValueNotifier(0); + + await tester.pumpWidget( + HookBuilder(builder: (c) { + final state = useState(false); + state.value = true; + final isPositive = use(IsPositiveHook(number)); + return Text('$isPositive', textDirection: TextDirection.ltr); + }), + ); + + expect(find.text('true'), findsOneWidget); + expect(find.text('false'), findsNothing); + + number.value = -1; + await tester.pump(); + + expect(find.text('false'), findsOneWidget); + expect(find.text('true'), findsNothing); + }); + + testWidgets('setState during build still allow mayHaveChange to abort builds', + (tester) async { + final number = ValueNotifier(0); + + var buildCount = 0; + + await tester.pumpWidget( + HookBuilder(builder: (c) { + buildCount++; + final state = useState(false); + state.value = true; + final isPositive = use(IsPositiveHook(number)); + return Text('$isPositive', textDirection: TextDirection.ltr); + }), + ); + + expect(find.text('true'), findsOneWidget); + expect(find.text('false'), findsNothing); + expect(buildCount, 1); + + number.value = 10; + await tester.pump(); + + expect(find.text('true'), findsOneWidget); + expect(find.text('false'), findsNothing); + expect(buildCount, 1); + }); + testWidgets('shouldRebuild defaults to true', (tester) async { MayRebuildState first; var buildCount = 0; From 7648d6dbe9570e549816f2253e0c45c73803a6bf Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Sat, 5 Sep 2020 15:18:30 +0200 Subject: [PATCH 179/384] format --- example/lib/use_reducer.dart | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/example/lib/use_reducer.dart b/example/lib/use_reducer.dart index 2b94c6fe..40d13ce2 100644 --- a/example/lib/use_reducer.dart +++ b/example/lib/use_reducer.dart @@ -6,7 +6,6 @@ import 'package:flutter_hooks/flutter_hooks.dart'; /// /// First, instead of a StatefulWidget, use a HookWidget instead! - // Create the State class State { int counter = 0; @@ -23,7 +22,7 @@ class UseReducerExample extends HookWidget { Widget build(BuildContext context) { // Create the reducer function that will handle the actions you dispatch State _reducer(State state, IncrementCounter action) { - if(action is IncrementCounter) { + if (action is IncrementCounter) { state.counter = state.counter + action.counter; } return state; @@ -32,10 +31,7 @@ class UseReducerExample extends HookWidget { // Next, invoke the `useReducer` function with the reducer funtion and initial state to create a // `_store` variable that contains the current state and dispatch. Whenever the value is // changed, this Widget will be rebuilt! - final _store = useReducer( - _reducer, - initialState: State() - ); + final _store = useReducer(_reducer, initialState: State()); return Scaffold( appBar: AppBar( From 253d1a64e314ec0069e777f2a31dd4c0f9980b40 Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Sat, 5 Sep 2020 18:04:48 +0200 Subject: [PATCH 180/384] rename the test file --- test/{focus_test.dart => use_focus_node_test.dart} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename test/{focus_test.dart => use_focus_node_test.dart} (100%) diff --git a/test/focus_test.dart b/test/use_focus_node_test.dart similarity index 100% rename from test/focus_test.dart rename to test/use_focus_node_test.dart From b067680945bf1762095b8331c8e7f00751370a5a Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Sat, 5 Sep 2020 18:12:57 +0200 Subject: [PATCH 181/384] Support parameter update & add more tests for FocusNode --- lib/src/focus.dart | 9 ++++ test/use_focus_node_test.dart | 79 ++++++++++++++++++++++++++++++++++- 2 files changed, 87 insertions(+), 1 deletion(-) diff --git a/lib/src/focus.dart b/lib/src/focus.dart index e58c5012..10a198bc 100644 --- a/lib/src/focus.dart +++ b/lib/src/focus.dart @@ -54,6 +54,15 @@ class _FocusNodeHookState extends HookState { ); } + @override + void didUpdateHook(_FocusNodeHook oldHook) { + _focusNode + ..debugLabel = hook.debugLabel + ..skipTraversal = hook.skipTraversal + ..canRequestFocus = hook.canRequestFocus + ..descendantsAreFocusable = hook.descendantsAreFocusable; + } + @override FocusNode build(BuildContext context) => _focusNode; diff --git a/test/use_focus_node_test.dart b/test/use_focus_node_test.dart index aec2bf7f..4e827773 100644 --- a/test/use_focus_node_test.dart +++ b/test/use_focus_node_test.dart @@ -4,7 +4,7 @@ import 'package:flutter_hooks/flutter_hooks.dart'; import 'mock.dart'; void main() { - testWidgets('creates a focus node', (tester) async { + testWidgets('creates a focus node and disposes it', (tester) async { FocusNode focusNode; await tester.pumpWidget( HookBuilder(builder: (_) { @@ -38,4 +38,81 @@ void main() { throwsAssertionError, ); }); + + testWidgets('default values matches with FocusNode', (tester) async { + final official = FocusNode(); + + FocusNode focusNode; + await tester.pumpWidget( + HookBuilder(builder: (_) { + focusNode = useFocusNode(); + return Container(); + }), + ); + + expect(focusNode.debugLabel, official.debugLabel); + expect(focusNode.onKey, official.onKey); + expect(focusNode.skipTraversal, official.skipTraversal); + expect(focusNode.canRequestFocus, official.canRequestFocus); + expect(focusNode.descendantsAreFocusable, official.descendantsAreFocusable); + }); + + testWidgets('has all the FocusNode parameters', (tester) async { + bool onKey(FocusNode node, RawKeyEvent event) => true; + + FocusNode focusNode; + await tester.pumpWidget( + HookBuilder(builder: (_) { + focusNode = useFocusNode( + debugLabel: 'Foo', + onKey: onKey, + skipTraversal: true, + canRequestFocus: false, + descendantsAreFocusable: false, + ); + return Container(); + }), + ); + + expect(focusNode.debugLabel, 'Foo'); + expect(focusNode.onKey, onKey); + expect(focusNode.skipTraversal, true); + expect(focusNode.canRequestFocus, false); + expect(focusNode.descendantsAreFocusable, false); + }); + + testWidgets('handles parameter change', (tester) async { + bool onKey(FocusNode node, RawKeyEvent event) => true; + bool onKey2(FocusNode node, RawKeyEvent event) => true; + + FocusNode focusNode; + await tester.pumpWidget( + HookBuilder(builder: (_) { + focusNode = useFocusNode( + debugLabel: 'Foo', + onKey: onKey, + skipTraversal: true, + canRequestFocus: false, + descendantsAreFocusable: false, + ); + return Container(); + }), + ); + + await tester.pumpWidget( + HookBuilder(builder: (_) { + focusNode = useFocusNode( + debugLabel: 'Bar', + onKey: onKey2, + ); + return Container(); + }), + ); + + expect(focusNode.onKey, onKey, reason: 'onKey has no setter'); + expect(focusNode.debugLabel, 'Bar'); + expect(focusNode.skipTraversal, false); + expect(focusNode.canRequestFocus, true); + expect(focusNode.descendantsAreFocusable, true); + }); } From 6f5327610f3b8dee99eac23fd92622d9cd96c1e1 Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Sat, 5 Sep 2020 18:15:40 +0200 Subject: [PATCH 182/384] Add changelog --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 417d53a6..a10e3f6d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ -## 0.13.2 +## [Unreleased] +- added all `FocusNode` parameters to `useFocusNode` - Fixed a bug where on hot-reload, a `HookWidget` could potentailly not rebuild ## 0.13.1 From ce89ea95d9c9ab8293ed3e9ad27e5af43df8dc30 Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Sun, 6 Sep 2020 18:19:35 +0200 Subject: [PATCH 183/384] closes #15 --- CHANGELOG.md | 2 + example/lib/main.dart | 4 +- lib/src/animation.dart | 37 ++++++++++++++- lib/src/async.dart | 12 +++++ lib/src/focus.dart | 3 ++ lib/src/framework.dart | 53 ++++++++++++++++++++-- lib/src/listenable.dart | 29 +++++++++++- lib/src/misc.dart | 33 +++++++++++++- lib/src/primitives.dart | 35 ++++++++++---- lib/src/scroll_controller.dart | 3 ++ lib/src/tab_controller.dart | 3 ++ lib/src/text_controller.dart | 3 ++ test/is_mounted_test.dart | 23 ++++++++++ test/memoized_test.dart | 17 +++---- test/use_animation_controller_test.dart | 34 +++++++++++++- test/use_animation_test.dart | 23 ++++++++++ test/use_effect_test.dart | 26 +++++++++++ test/use_focus_node_test.dart | 23 ++++++++++ test/use_future_test.dart | 28 ++++++++++++ test/use_listenable_test.dart | 24 ++++++++++ test/use_previous_test.dart | 30 ++++++++++++ test/use_reassemble_test.dart | 23 ++++++++++ test/use_reducer_test.dart | 23 ++++++++++ test/use_scroll_controller_test.dart | 23 ++++++++++ test/use_state_test.dart | 18 +++----- test/use_stream_controller_test.dart | 27 +++++++++++ test/use_stream_test.dart | 27 +++++++++++ test/use_tab_controller_test.dart | 26 +++++++++++ test/use_text_editing_controller_test.dart | 28 ++++++++++++ test/use_ticker_provider_test.dart | 25 ++++++++++ test/use_value_changed_test.dart | 31 +++++++++++++ test/use_value_listenable_test.dart | 23 ++++++++++ test/use_value_notifier_test.dart | 23 ++++++++++ 33 files changed, 701 insertions(+), 41 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a10e3f6d..1ca08796 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ - added all `FocusNode` parameters to `useFocusNode` - Fixed a bug where on hot-reload, a `HookWidget` could potentailly not rebuild +- Allow hooks to integrate with the devtool using the `Diagnosticable` API, and + implement it for all built-in hooks. ## 0.13.1 diff --git a/example/lib/main.dart b/example/lib/main.dart index 9570ada7..58c40afa 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -1,5 +1,6 @@ // ignore_for_file: omit_local_variable_types import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; import 'star_wars/planet_screen.dart'; import 'use_effect.dart'; @@ -11,9 +12,10 @@ void main() => runApp(HooksGalleryApp()); /// An App that demonstrates how to use hooks. It includes examples that cover /// the hooks provided by this library as well as examples that demonstrate /// how to write custom hooks. -class HooksGalleryApp extends StatelessWidget { +class HooksGalleryApp extends HookWidget { @override Widget build(BuildContext context) { + useAnimationController(duration: const Duration(seconds: 2)); return MaterialApp( title: 'Flutter Hooks Gallery', home: Scaffold( diff --git a/lib/src/animation.dart b/lib/src/animation.dart index 53f2bb20..4c9eacd8 100644 --- a/lib/src/animation.dart +++ b/lib/src/animation.dart @@ -6,10 +6,27 @@ part of 'hooks.dart'; /// * [Animation] /// * [useValueListenable], [useListenable], [useStream] T useAnimation(Animation animation) { - useListenable(animation); + use(_UseAnimationHook(animation)); return animation.value; } +class _UseAnimationHook extends _ListenableHook { + const _UseAnimationHook(Animation animation) : super(animation); + + @override + _UseAnimationStateHook createState() { + return _UseAnimationStateHook(); + } +} + +class _UseAnimationStateHook extends _ListenableStateHook { + @override + String get debugLabel => 'useAnimation'; + + @override + Object get debugValue => (hook.listenable as Animation).value; +} + /// Creates an [AnimationController] automatically disposed. /// /// If no [vsync] is provided, the [TickerProvider] is implicitly obtained using [useSingleTickerProvider]. @@ -72,6 +89,12 @@ class _AnimationControllerHook extends Hook { @override _AnimationControllerHookState createState() => _AnimationControllerHookState(); + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(DiagnosticsProperty('duration', duration)); + } } class _AnimationControllerHookState @@ -113,6 +136,12 @@ class _AnimationControllerHookState void dispose() { _animationController.dispose(); } + + @override + bool get debugHasShortDescription => false; + + @override + String get debugLabel => 'useAnimationController'; } /// Creates a single usage [TickerProvider]. @@ -176,4 +205,10 @@ class _TickerProviderHookState } return this; } + + @override + String get debugLabel => 'useSingleTickerProvider'; + + @override + bool get debugSkipValue => true; } diff --git a/lib/src/async.dart b/lib/src/async.dart index 842f07ff..15d4520a 100644 --- a/lib/src/async.dart +++ b/lib/src/async.dart @@ -91,6 +91,12 @@ class _FutureStateHook extends HookState, _FutureHook> { AsyncSnapshot build(BuildContext context) { return _snapshot; } + + @override + String get debugLabel => 'useFuture'; + + @override + Object get debugValue => _snapshot; } /// Subscribes to a [Stream] and return its current state in an [AsyncSnapshot]. @@ -201,6 +207,9 @@ class _StreamHookState extends HookState, _StreamHook> { AsyncSnapshot afterDisconnected(AsyncSnapshot current) => current.inState(ConnectionState.none); + + @override + String get debugLabel => 'useStream'; } /// Creates a [StreamController] automatically disposed. @@ -269,4 +278,7 @@ class _StreamControllerHookState void dispose() { _controller.close(); } + + @override + String get debugLabel => 'useStreamController'; } diff --git a/lib/src/focus.dart b/lib/src/focus.dart index 10a198bc..8db92254 100644 --- a/lib/src/focus.dart +++ b/lib/src/focus.dart @@ -68,4 +68,7 @@ class _FocusNodeHookState extends HookState { @override void dispose() => _focusNode?.dispose(); + + @override + String get debugLabel => 'useFocusNode'; } diff --git a/lib/src/framework.dart b/lib/src/framework.dart index f435aea1..a9fd32c7 100644 --- a/lib/src/framework.dart +++ b/lib/src/framework.dart @@ -127,7 +127,7 @@ R use(Hook hook) => Hook.use(hook); /// In fact this has secondary bonus: `duration` is kept updated with the latest value. /// If we were to pass a variable as `duration` instead of a constant, then on value change the [AnimationController] will be updated. @immutable -abstract class Hook { +abstract class Hook with Diagnosticable { /// Allows subclasses to have a `const` constructor const Hook({this.keys}); @@ -209,6 +209,22 @@ abstract class HookState> with Diagnosticable { BuildContext get context => _element; HookElement _element; + R _debugLastBuiltValue; + + /// The value shown in the devtool. + /// + /// Defaults to the last value returned by [build]. + Object get debugValue => _debugLastBuiltValue; + + /// A flag to not show [debugValue] in the devtool, for hooks that returns nothing. + bool get debugSkipValue => false; + + /// A label used by the devtool to show the state of a hook + String get debugLabel => null; + + /// Whether the devtool description should skip [debugFillProperties] or not. + bool get debugHasShortDescription => true; + /// Equivalent of [State.widget] for [HookState] T get hook => _hook; T _hook; @@ -282,6 +298,16 @@ abstract class HookState> with Diagnosticable { .._isOptionalRebuild = false ..markNeedsBuild(); } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + final value = debugValue; + if (value != this) { + properties.add(DiagnosticsProperty(null, value)); + } + hook.debugFillProperties(properties); + } } class _Entry extends LinkedListEntry<_Entry> { @@ -447,6 +473,10 @@ Type mismatch between hooks: } final result = _currentHookState.value.build(this) as R; + assert(() { + _currentHookState.value._debugLastBuiltValue = result; + return true; + }(), ''); _currentHookState = _currentHookState.next; return result; } @@ -509,13 +539,28 @@ Type mismatch between hooks: super.deactivate(); } - // ignore: comment_references - /// Add properties [properties] using every [HookState.debugFillProperties] @override void debugFillProperties(DiagnosticPropertiesBuilder properties) { super.debugFillProperties(properties); for (final hookState in debugHooks) { - hookState.debugFillProperties(properties); + if (hookState.debugHasShortDescription) { + if (hookState.debugSkipValue) { + properties.add( + StringProperty(hookState.debugLabel, '', ifEmpty: ''), + ); + } else { + properties.add( + DiagnosticsProperty( + hookState.debugLabel, + hookState.debugValue, + ), + ); + } + } else { + properties.add( + DiagnosticsProperty(hookState.debugLabel, hookState), + ); + } } } } diff --git a/lib/src/listenable.dart b/lib/src/listenable.dart index fc678fef..d1a44ca6 100644 --- a/lib/src/listenable.dart +++ b/lib/src/listenable.dart @@ -6,7 +6,25 @@ part of 'hooks.dart'; /// * [ValueListenable], the created object /// * [useListenable] T useValueListenable(ValueListenable valueListenable) { - return useListenable(valueListenable).value; + use(_UseValueListenableHook(valueListenable)); + return valueListenable.value; +} + +class _UseValueListenableHook extends _ListenableHook { + const _UseValueListenableHook(ValueListenable animation) : super(animation); + + @override + _UseValueListenableStateHook createState() { + return _UseValueListenableStateHook(); + } +} + +class _UseValueListenableStateHook extends _ListenableStateHook { + @override + String get debugLabel => 'useValueListenable'; + + @override + Object get debugValue => (hook.listenable as ValueListenable).value; } /// Subscribes to a [Listenable] and mark the widget as needing build @@ -57,6 +75,12 @@ class _ListenableStateHook extends HookState { void dispose() { hook.listenable.removeListener(_listener); } + + @override + String get debugLabel => 'useListenable'; + + @override + Object get debugValue => hook.listenable; } /// Creates a [ValueNotifier] automatically disposed. @@ -103,4 +127,7 @@ class _UseValueNotiferHookState void dispose() { notifier.dispose(); } + + @override + String get debugLabel => 'useValueNotifier'; } diff --git a/lib/src/misc.dart b/lib/src/misc.dart index bc803202..20aeca1f 100644 --- a/lib/src/misc.dart +++ b/lib/src/misc.dart @@ -38,8 +38,13 @@ Store useReducer( State initialState, Action initialAction, }) { - return use(_ReducerdHook(reducer, - initialAction: initialAction, initialState: initialState)); + return use( + _ReducerdHook( + reducer, + initialAction: initialAction, + initialState: initialState, + ), + ); } class _ReducerdHook extends Hook> { @@ -82,6 +87,12 @@ class _ReducerdHookState Store build(BuildContext context) { return this; } + + @override + String get debugLabel => 'useReducer'; + + @override + Object get debugValue => state; } /// Returns the previous argument called to [usePrevious]. @@ -108,6 +119,12 @@ class _PreviousHookState extends HookState> { @override T build(BuildContext context) => previous; + + @override + String get debugLabel => 'usePrevious'; + + @override + Object get debugValue => previous; } /// Runs the callback on every hot reload @@ -142,6 +159,12 @@ class _ReassembleHookState extends HookState { @override void build(BuildContext context) {} + + @override + String get debugLabel => 'useReassemble'; + + @override + bool get debugSkipValue => true; } /// Returns an [IsMounted] object that you can use @@ -185,6 +208,12 @@ class _IsMountedHookState extends HookState { _mounted = false; super.dispose(); } + + @override + String get debugLabel => 'useIsMounted'; + + @override + Object get debugValue => _mounted; } typedef IsMounted = bool Function(); diff --git a/lib/src/primitives.dart b/lib/src/primitives.dart index 7dd7c12c..fa43850b 100644 --- a/lib/src/primitives.dart +++ b/lib/src/primitives.dart @@ -43,10 +43,7 @@ class _MemoizedHookState extends HookState> { } @override - void debugFillProperties(DiagnosticPropertiesBuilder properties) { - super.debugFillProperties(properties); - properties.add(DiagnosticsProperty('useMemoized<$T>', value)); - } + String get debugLabel => 'useMemoized<$T>'; } /// Watches a value and calls a callback whenever the value changed. @@ -102,6 +99,22 @@ class _ValueChangedHookState R build(BuildContext context) { return _result; } + + @override + String get debugLabel => 'useValueChanged'; + + @override + bool get debugHasShortDescription => false; + + @override + bool get debugSkipValue => true; + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(DiagnosticsProperty('value', hook.value)); + properties.add(DiagnosticsProperty('result', _result)); + } } typedef Dispose = void Function(); @@ -182,6 +195,12 @@ class _EffectHookState extends HookState { void scheduleEffect() { disposer = hook.effect(); } + + @override + String get debugLabel => 'useEffect'; + + @override + bool get debugSkipValue => true; } /// Create variable and subscribes to it. @@ -249,8 +268,8 @@ class _StateHookState extends HookState, _StateHook> { } @override - void debugFillProperties(DiagnosticPropertiesBuilder properties) { - super.debugFillProperties(properties); - properties.add(DiagnosticsProperty('useState<$T>', _state.value)); - } + Object get debugValue => _state.value; + + @override + String get debugLabel => 'useState<$T>'; } diff --git a/lib/src/scroll_controller.dart b/lib/src/scroll_controller.dart index 93c12a66..b7e98741 100644 --- a/lib/src/scroll_controller.dart +++ b/lib/src/scroll_controller.dart @@ -55,4 +55,7 @@ class _ScrollControllerHookState @override void dispose() => controller.dispose(); + + @override + String get debugLabel => 'useScrollController'; } diff --git a/lib/src/tab_controller.dart b/lib/src/tab_controller.dart index 6952b504..e825dceb 100644 --- a/lib/src/tab_controller.dart +++ b/lib/src/tab_controller.dart @@ -55,4 +55,7 @@ class _TabControllerHookState @override void dispose() => controller?.dispose(); + + @override + String get debugLabel => 'useTabController'; } diff --git a/lib/src/text_controller.dart b/lib/src/text_controller.dart index 67081311..96181923 100644 --- a/lib/src/text_controller.dart +++ b/lib/src/text_controller.dart @@ -94,4 +94,7 @@ class _TextEditingControllerHookState @override void dispose() => _controller?.dispose(); + + @override + String get debugLabel => 'useTextEditingController'; } diff --git a/test/is_mounted_test.dart b/test/is_mounted_test.dart index c30828f0..68670616 100644 --- a/test/is_mounted_test.dart +++ b/test/is_mounted_test.dart @@ -1,3 +1,4 @@ +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -19,4 +20,26 @@ void main() { expect(isMounted(), false); }); + + testWidgets('debugFillProperties', (tester) async { + await tester.pumpWidget( + HookBuilder(builder: (context) { + useIsMounted(); + return const SizedBox(); + }), + ); + + final element = tester.element(find.byType(HookBuilder)); + + expect( + element + .toDiagnosticsNode(style: DiagnosticsTreeStyle.offstage) + .toStringDeep(), + equalsIgnoringHashCodes( + 'HookBuilder\n' + ' │ useIsMounted: true\n' + ' └SizedBox(renderObject: RenderConstrainedBox#00000)\n', + ), + ); + }); } diff --git a/test/memoized_test.dart b/test/memoized_test.dart index efc0e46a..d1ff1acf 100644 --- a/test/memoized_test.dart +++ b/test/memoized_test.dart @@ -1,5 +1,3 @@ -import 'dart:math'; - import 'package:flutter/widgets.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; @@ -284,19 +282,16 @@ void main() { verifyNoMoreInteractions(valueBuilder); }); - testWidgets('debugFillProperties should print memoized hook ', - (tester) async { - HookElement element; - - final hookWidget = HookBuilder( - builder: (context) { - element = context as HookElement; + testWidgets('debugFillProperties', (tester) async { + await tester.pumpWidget( + HookBuilder(builder: (context) { useMemoized>(() => Future.value(10)); useMemoized(() => 43); return const SizedBox(); - }, + }), ); - await tester.pumpWidget(hookWidget); + + final element = tester.element(find.byType(HookBuilder)); expect( element diff --git a/test/use_animation_controller_test.dart b/test/use_animation_controller_test.dart index d950f84b..bf03feaf 100644 --- a/test/use_animation_controller_test.dart +++ b/test/use_animation_controller_test.dart @@ -1,4 +1,4 @@ -import 'package:flutter/scheduler.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; @@ -31,6 +31,38 @@ void main() { await tester.pumpWidget(const SizedBox()); }); + testWidgets('diagnostics', (tester) async { + await tester.pumpWidget( + HookBuilder(builder: (context) { + useAnimationController( + animationBehavior: AnimationBehavior.preserve, + duration: const Duration(seconds: 1), + initialValue: 42, + lowerBound: 24, + upperBound: 84, + debugLabel: 'Foo', + ); + return const SizedBox(); + }), + ); + + final element = tester.element(find.byType(HookBuilder)); + + expect( + element + .toDiagnosticsNode(style: DiagnosticsTreeStyle.offstage) + .toStringDeep(), + equalsIgnoringHashCodes( + 'HookBuilder\n' + ' │ useSingleTickerProvider\n' + ' │ useAnimationController:\n' + ' │ _AnimationControllerHookState#00000(AnimationController#00000(▶\n' + ' │ 42.000; paused; for Foo), duration: 0:00:01.000000)\n' + ' └SizedBox(renderObject: RenderConstrainedBox#00000)\n', + ), + ); + }); + testWidgets('useAnimationController complex', (tester) async { AnimationController controller; diff --git a/test/use_animation_test.dart b/test/use_animation_test.dart index 8e29ff50..a5ba936f 100644 --- a/test/use_animation_test.dart +++ b/test/use_animation_test.dart @@ -1,3 +1,4 @@ +import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; @@ -15,6 +16,28 @@ void main() { expect(tester.takeException(), isAssertionError); }); + testWidgets('debugFillProperties', (tester) async { + await tester.pumpWidget( + HookBuilder(builder: (context) { + useAnimation(const AlwaysStoppedAnimation(42)); + return const SizedBox(); + }), + ); + + final element = tester.element(find.byType(HookBuilder)); + + expect( + element + .toDiagnosticsNode(style: DiagnosticsTreeStyle.offstage) + .toStringDeep(), + equalsIgnoringHashCodes( + 'HookBuilder\n' + ' │ useAnimation: 42\n' + ' └SizedBox(renderObject: RenderConstrainedBox#00000)\n', + ), + ); + }); + testWidgets('useAnimation', (tester) async { var listenable = AnimationController(vsync: tester); double result; diff --git a/test/use_effect_test.dart b/test/use_effect_test.dart index 0a7c50eb..29a692ae 100644 --- a/test/use_effect_test.dart +++ b/test/use_effect_test.dart @@ -1,3 +1,4 @@ +import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; @@ -21,6 +22,31 @@ void main() { reset(unrelated); reset(effect); }); + + testWidgets('debugFillProperties', (tester) async { + await tester.pumpWidget( + HookBuilder(builder: (context) { + useEffect(() { + return null; + }, []); + return const SizedBox(); + }), + ); + + final element = tester.element(find.byType(HookBuilder)); + + expect( + element + .toDiagnosticsNode(style: DiagnosticsTreeStyle.offstage) + .toStringDeep(), + equalsIgnoringHashCodes( + 'HookBuilder\n' + ' │ useEffect\n' + ' └SizedBox(renderObject: RenderConstrainedBox#00000)\n', + ), + ); + }); + testWidgets('useEffect null callback throws', (tester) async { await tester.pumpWidget( HookBuilder(builder: (c) { diff --git a/test/use_focus_node_test.dart b/test/use_focus_node_test.dart index 4e827773..e939f256 100644 --- a/test/use_focus_node_test.dart +++ b/test/use_focus_node_test.dart @@ -1,3 +1,4 @@ +import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; @@ -39,6 +40,28 @@ void main() { ); }); + testWidgets('debugFillProperties', (tester) async { + await tester.pumpWidget( + HookBuilder(builder: (context) { + useFocusNode(); + return const SizedBox(); + }), + ); + + final element = tester.element(find.byType(HookBuilder)); + + expect( + element + .toDiagnosticsNode(style: DiagnosticsTreeStyle.offstage) + .toStringDeep(), + equalsIgnoringHashCodes( + 'HookBuilder\n' + ' │ useFocusNode: FocusNode#00000\n' + ' └SizedBox(renderObject: RenderConstrainedBox#00000)\n', + ), + ); + }); + testWidgets('default values matches with FocusNode', (tester) async { final official = FocusNode(); diff --git a/test/use_future_test.dart b/test/use_future_test.dart index e82f3377..60e5bdc8 100644 --- a/test/use_future_test.dart +++ b/test/use_future_test.dart @@ -1,5 +1,6 @@ import 'dart:async'; +import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; @@ -28,6 +29,33 @@ void main() { await tester.pumpWidget(HookBuilder(builder: builder(future))); expect(value.data, 42); }); + + testWidgets('debugFillProperties', (tester) async { + final future = Future.value(42); + + await tester.pumpWidget( + HookBuilder(builder: (context) { + useFuture(future); + return const SizedBox(); + }), + ); + + await tester.pump(); + + final element = tester.element(find.byType(HookBuilder)); + + expect( + element + .toDiagnosticsNode(style: DiagnosticsTreeStyle.offstage) + .toStringDeep(), + equalsIgnoringHashCodes( + 'HookBuilder\n' + ' │ useFuture: AsyncSnapshot(ConnectionState.done, 42, null)\n' + ' └SizedBox(renderObject: RenderConstrainedBox#00000)\n', + ), + ); + }); + testWidgets('If preserveState == false, changing future resets value', (tester) async { AsyncSnapshot value; diff --git a/test/use_listenable_test.dart b/test/use_listenable_test.dart index b9992611..2439d716 100644 --- a/test/use_listenable_test.dart +++ b/test/use_listenable_test.dart @@ -1,3 +1,4 @@ +import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; @@ -16,6 +17,29 @@ void main() { expect(tester.takeException(), isAssertionError); }); + + testWidgets('debugFillProperties', (tester) async { + await tester.pumpWidget( + HookBuilder(builder: (context) { + useListenable(const AlwaysStoppedAnimation(42)); + return const SizedBox(); + }), + ); + + final element = tester.element(find.byType(HookBuilder)); + + expect( + element + .toDiagnosticsNode(style: DiagnosticsTreeStyle.offstage) + .toStringDeep(), + equalsIgnoringHashCodes( + 'HookBuilder\n' + ' │ useListenable: AlwaysStoppedAnimation#00000(▶ 42; paused)\n' + ' └SizedBox(renderObject: RenderConstrainedBox#00000)\n', + ), + ); + }); + testWidgets('useListenable', (tester) async { var listenable = ValueNotifier(0); diff --git a/test/use_previous_test.dart b/test/use_previous_test.dart index 0738fdfa..de653de1 100644 --- a/test/use_previous_test.dart +++ b/test/use_previous_test.dart @@ -1,3 +1,4 @@ +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -30,4 +31,33 @@ void main() { expect(find.text('2'), findsOneWidget); }); }); + + testWidgets('debugFillProperties', (tester) async { + await tester.pumpWidget( + HookBuilder(builder: (context) { + usePrevious(42); + return const SizedBox(); + }), + ); + + await tester.pumpWidget( + HookBuilder(builder: (context) { + usePrevious(21); + return const SizedBox(); + }), + ); + + final element = tester.element(find.byType(HookBuilder)); + + expect( + element + .toDiagnosticsNode(style: DiagnosticsTreeStyle.offstage) + .toStringDeep(), + equalsIgnoringHashCodes( + 'HookBuilder\n' + ' │ usePrevious: 42\n' + ' └SizedBox(renderObject: RenderConstrainedBox#00000)\n', + ), + ); + }); } diff --git a/test/use_reassemble_test.dart b/test/use_reassemble_test.dart index aec7c17d..dab6e519 100644 --- a/test/use_reassemble_test.dart +++ b/test/use_reassemble_test.dart @@ -1,3 +1,4 @@ +import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; @@ -31,4 +32,26 @@ void main() { verify(reassemble()).called(1); verifyNoMoreInteractions(reassemble); }); + + testWidgets('debugFillProperties', (tester) async { + await tester.pumpWidget( + HookBuilder(builder: (context) { + useReassemble(() {}); + return const SizedBox(); + }), + ); + + final element = tester.element(find.byType(HookBuilder)); + + expect( + element + .toDiagnosticsNode(style: DiagnosticsTreeStyle.offstage) + .toStringDeep(), + equalsIgnoringHashCodes( + 'HookBuilder\n' + ' │ useReassemble\n' + ' └SizedBox(renderObject: RenderConstrainedBox#00000)\n', + ), + ); + }); } diff --git a/test/use_reducer_test.dart b/test/use_reducer_test.dart index 5aa34161..5b31a2ec 100644 --- a/test/use_reducer_test.dart +++ b/test/use_reducer_test.dart @@ -1,9 +1,32 @@ +import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'mock.dart'; void main() { + testWidgets('debugFillProperties', (tester) async { + await tester.pumpWidget( + HookBuilder(builder: (context) { + useReducer((state, action) => 42); + return const SizedBox(); + }), + ); + + final element = tester.element(find.byType(HookBuilder)); + + expect( + element + .toDiagnosticsNode(style: DiagnosticsTreeStyle.offstage) + .toStringDeep(), + equalsIgnoringHashCodes( + 'HookBuilder\n' + ' │ useReducer: 42\n' + ' └SizedBox(renderObject: RenderConstrainedBox#00000)\n', + ), + ); + }); + group('useReducer', () { testWidgets('basic', (tester) async { final reducer = MockReducer(); diff --git a/test/use_scroll_controller_test.dart b/test/use_scroll_controller_test.dart index 1287c4b9..b926ab1c 100644 --- a/test/use_scroll_controller_test.dart +++ b/test/use_scroll_controller_test.dart @@ -1,3 +1,4 @@ +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -7,6 +8,28 @@ import 'package:flutter_hooks/src/hooks.dart'; import 'mock.dart'; void main() { + testWidgets('debugFillProperties', (tester) async { + await tester.pumpWidget( + HookBuilder(builder: (context) { + useScrollController(); + return const SizedBox(); + }), + ); + + final element = tester.element(find.byType(HookBuilder)); + + expect( + element + .toDiagnosticsNode(style: DiagnosticsTreeStyle.offstage) + .toStringDeep(), + equalsIgnoringHashCodes( + 'HookBuilder\n' + ' │ useScrollController: ScrollController#00000(no clients)\n' + ' └SizedBox(renderObject: RenderConstrainedBox#00000)\n', + ), + ); + }); + group('useScrollController', () { testWidgets('initial values matches with real constructor', (tester) async { ScrollController controller; diff --git a/test/use_state_test.dart b/test/use_state_test.dart index 0e2aae1f..c42240f3 100644 --- a/test/use_state_test.dart +++ b/test/use_state_test.dart @@ -87,13 +87,10 @@ void main() { await tester.pumpWidget(hookWidget); expect( - element - .toDiagnosticsNode(style: DiagnosticsTreeStyle.offstage) - .toStringDeep(), + element.toStringDeep(), equalsIgnoringHashCodes( - 'HookBuilder\n' - ' │ useState: 0\n' - ' └SizedBox(renderObject: RenderConstrainedBox#00000)\n', + 'HookBuilder(useState: 0)\n' + '└SizedBox(renderObject: RenderConstrainedBox#00000)\n', ), ); @@ -102,13 +99,10 @@ void main() { await tester.pump(); expect( - element - .toDiagnosticsNode(style: DiagnosticsTreeStyle.offstage) - .toStringDeep(), + element.toStringDeep(), equalsIgnoringHashCodes( - 'HookBuilder\n' - ' │ useState: 1\n' - ' └SizedBox(renderObject: RenderConstrainedBox#00000)\n', + 'HookBuilder(useState: 1)\n' + '└SizedBox(renderObject: RenderConstrainedBox#00000)\n', ), ); }); diff --git a/test/use_stream_controller_test.dart b/test/use_stream_controller_test.dart index 782af5c7..2950342d 100644 --- a/test/use_stream_controller_test.dart +++ b/test/use_stream_controller_test.dart @@ -1,11 +1,38 @@ import 'dart:async'; +import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'mock.dart'; void main() { + testWidgets('debugFillProperties', (tester) async { + await tester.pumpWidget( + HookBuilder(builder: (context) { + useStreamController(); + return const SizedBox(); + }), + ); + + await tester.pump(); + + final element = tester.element(find.byType(HookBuilder)); + + expect( + element + .toDiagnosticsNode(style: DiagnosticsTreeStyle.offstage) + .toStringDeep(), + equalsIgnoringHashCodes( + 'HookBuilder\n' + ' │ useStreamController: Instance of\n' + // ignore: avoid_escaping_inner_quotes + ' │ \'_AsyncBroadcastStreamController\'\n' + ' └SizedBox(renderObject: RenderConstrainedBox#00000)\n', + ), + ); + }); + group('useStreamController', () { testWidgets('keys', (tester) async { StreamController controller; diff --git a/test/use_stream_test.dart b/test/use_stream_test.dart index 2249592c..46a83ff0 100644 --- a/test/use_stream_test.dart +++ b/test/use_stream_test.dart @@ -2,6 +2,7 @@ import 'dart:async'; +import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; @@ -10,6 +11,32 @@ import 'mock.dart'; /// port of [StreamBuilder] /// void main() { + testWidgets('debugFillProperties', (tester) async { + final stream = Stream.value(42); + + await tester.pumpWidget( + HookBuilder(builder: (context) { + useStream(stream); + return const SizedBox(); + }), + ); + + await tester.pump(); + + final element = tester.element(find.byType(HookBuilder)); + + expect( + element + .toDiagnosticsNode(style: DiagnosticsTreeStyle.offstage) + .toStringDeep(), + equalsIgnoringHashCodes( + 'HookBuilder\n' + ' │ useStream: AsyncSnapshot(ConnectionState.done, 42, null)\n' + ' └SizedBox(renderObject: RenderConstrainedBox#00000)\n', + ), + ); + }); + testWidgets('default preserve state, changing stream keeps previous value', (tester) async { AsyncSnapshot value; diff --git a/test/use_tab_controller_test.dart b/test/use_tab_controller_test.dart index 17efcdac..1a66215e 100644 --- a/test/use_tab_controller_test.dart +++ b/test/use_tab_controller_test.dart @@ -1,3 +1,4 @@ +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -7,6 +8,31 @@ import 'package:flutter_hooks/src/hooks.dart'; import 'mock.dart'; void main() { + testWidgets('debugFillProperties', (tester) async { + await tester.pumpWidget( + HookBuilder(builder: (context) { + useTabController(initialLength: 4); + return const SizedBox(); + }), + ); + + await tester.pump(); + + final element = tester.element(find.byType(HookBuilder)); + + expect( + element + .toDiagnosticsNode(style: DiagnosticsTreeStyle.offstage) + .toStringDeep(), + equalsIgnoringHashCodes( + 'HookBuilder\n' + ' │ useSingleTickerProvider\n' + " │ useTabController: Instance of 'TabController'\n" + ' └SizedBox(renderObject: RenderConstrainedBox#00000)\n', + ), + ); + }); + group('useTabController', () { testWidgets('initial values matches with real constructor', (tester) async { TabController controller; diff --git a/test/use_text_editing_controller_test.dart b/test/use_text_editing_controller_test.dart index 0464140e..2a5612fb 100644 --- a/test/use_text_editing_controller_test.dart +++ b/test/use_text_editing_controller_test.dart @@ -7,6 +7,34 @@ import 'package:flutter_test/flutter_test.dart'; import 'mock.dart'; void main() { + testWidgets('debugFillProperties', (tester) async { + await tester.pumpWidget( + HookBuilder(builder: (context) { + useTextEditingController(); + return const SizedBox(); + }), + ); + + await tester.pump(); + + final element = tester.element(find.byType(HookBuilder)); + + expect( + element + .toDiagnosticsNode(style: DiagnosticsTreeStyle.offstage) + .toStringDeep(), + equalsIgnoringHashCodes( + 'HookBuilder\n' + ' │ useTextEditingController:\n' + ' │ TextEditingController#00000(TextEditingValue(text: ┤├,\n' + ' │ selection: TextSelection(baseOffset: -1, extentOffset: -1,\n' + ' │ affinity: TextAffinity.downstream, isDirectional: false),\n' + ' │ composing: TextRange(start: -1, end: -1)))\n' + ' └SizedBox(renderObject: RenderConstrainedBox#00000)\n', + ), + ); + }); + testWidgets('useTextEditingController returns a controller', (tester) async { final rebuilder = ValueNotifier(0); TextEditingController controller; diff --git a/test/use_ticker_provider_test.dart b/test/use_ticker_provider_test.dart index be7ff0dc..c9548b67 100644 --- a/test/use_ticker_provider_test.dart +++ b/test/use_ticker_provider_test.dart @@ -1,3 +1,4 @@ +import 'package:flutter/foundation.dart'; import 'package:flutter/scheduler.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; @@ -5,6 +6,30 @@ import 'package:flutter_hooks/flutter_hooks.dart'; import 'mock.dart'; void main() { + testWidgets('debugFillProperties', (tester) async { + await tester.pumpWidget( + HookBuilder(builder: (context) { + useSingleTickerProvider(); + return const SizedBox(); + }), + ); + + await tester.pump(); + + final element = tester.element(find.byType(HookBuilder)); + + expect( + element + .toDiagnosticsNode(style: DiagnosticsTreeStyle.offstage) + .toStringDeep(), + equalsIgnoringHashCodes( + 'HookBuilder\n' + ' │ useSingleTickerProvider\n' + ' └SizedBox(renderObject: RenderConstrainedBox#00000)\n', + ), + ); + }); + testWidgets('useSingleTickerProvider basic', (tester) async { TickerProvider provider; diff --git a/test/use_value_changed_test.dart b/test/use_value_changed_test.dart index 41cf0247..a755df45 100644 --- a/test/use_value_changed_test.dart +++ b/test/use_value_changed_test.dart @@ -1,9 +1,40 @@ +import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'mock.dart'; void main() { + testWidgets('diagnostics', (tester) async { + await tester.pumpWidget( + HookBuilder(builder: (context) { + useValueChanged(0, (_, __) => 21); + return const SizedBox(); + }), + ); + + await tester.pumpWidget( + HookBuilder(builder: (context) { + useValueChanged(42, (_, __) => 21); + return const SizedBox(); + }), + ); + + final element = tester.element(find.byType(HookBuilder)); + + expect( + element + .toDiagnosticsNode(style: DiagnosticsTreeStyle.offstage) + .toStringDeep(), + equalsIgnoringHashCodes( + 'HookBuilder\n' + ' │ useValueChanged: _ValueChangedHookState#00000(21,\n' + ' │ value: 42, result: 21)\n' + ' └SizedBox(renderObject: RenderConstrainedBox#00000)\n', + ), + ); + }); + testWidgets('useValueChanged basic', (tester) async { var value = 42; final _useValueChanged = MockValueChanged(); diff --git a/test/use_value_listenable_test.dart b/test/use_value_listenable_test.dart index 640034c4..821bcc1a 100644 --- a/test/use_value_listenable_test.dart +++ b/test/use_value_listenable_test.dart @@ -1,9 +1,32 @@ +import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'mock.dart'; void main() { + testWidgets('diagnostics', (tester) async { + await tester.pumpWidget( + HookBuilder(builder: (context) { + useValueListenable(ValueNotifier(0)); + return const SizedBox(); + }), + ); + + final element = tester.element(find.byType(HookBuilder)); + + expect( + element + .toDiagnosticsNode(style: DiagnosticsTreeStyle.offstage) + .toStringDeep(), + equalsIgnoringHashCodes( + 'HookBuilder\n' + ' │ useValueListenable: 0\n' + ' └SizedBox(renderObject: RenderConstrainedBox#00000)\n', + ), + ); + }); + testWidgets('useValueListenable throws with null', (tester) async { await tester.pumpWidget( HookBuilder( diff --git a/test/use_value_notifier_test.dart b/test/use_value_notifier_test.dart index 1a0c4f48..bad9ee8f 100644 --- a/test/use_value_notifier_test.dart +++ b/test/use_value_notifier_test.dart @@ -1,9 +1,32 @@ +import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'mock.dart'; void main() { + testWidgets('diagnostics', (tester) async { + await tester.pumpWidget( + HookBuilder(builder: (context) { + useValueNotifier(0); + return const SizedBox(); + }), + ); + + final element = tester.element(find.byType(HookBuilder)); + + expect( + element + .toDiagnosticsNode(style: DiagnosticsTreeStyle.offstage) + .toStringDeep(), + equalsIgnoringHashCodes( + 'HookBuilder\n' + ' │ useValueNotifier: ValueNotifier#00000(0)\n' + ' └SizedBox(renderObject: RenderConstrainedBox#00000)\n', + ), + ); + }); + group('useValueNotifier', () { testWidgets('useValueNotifier basic', (tester) async { ValueNotifier state; From 04d9eafdf68857fc807d421c60fef56fc28fc559 Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Sun, 6 Sep 2020 18:40:39 +0200 Subject: [PATCH 184/384] Remove dead code --- lib/src/primitives.dart | 3 --- 1 file changed, 3 deletions(-) diff --git a/lib/src/primitives.dart b/lib/src/primitives.dart index fa43850b..fd3c0c2f 100644 --- a/lib/src/primitives.dart +++ b/lib/src/primitives.dart @@ -106,9 +106,6 @@ class _ValueChangedHookState @override bool get debugHasShortDescription => false; - @override - bool get debugSkipValue => true; - @override void debugFillProperties(DiagnosticPropertiesBuilder properties) { super.debugFillProperties(properties); From 8c566cfc1fa6cb2ef96068d153653a4448c4e829 Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Sun, 6 Sep 2020 18:53:30 +0200 Subject: [PATCH 185/384] 0.14.0 --- CHANGELOG.md | 2 +- pubspec.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1ca08796..77049b9e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,4 @@ -## [Unreleased] +## [0.14.0] - added all `FocusNode` parameters to `useFocusNode` - Fixed a bug where on hot-reload, a `HookWidget` could potentailly not rebuild diff --git a/pubspec.yaml b/pubspec.yaml index 6b127061..2c1983b5 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: flutter_hooks description: A flutter implementation of React hooks. It adds a new kind of widget with enhanced code reuse. homepage: https://github.com/rrousselGit/flutter_hooks -version: 0.13.2 +version: 0.14.0 environment: sdk: ">=2.7.0 <3.0.0" From d8a37d432e00b4738bdda52ee1156baa95a394bf Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Sat, 19 Sep 2020 14:47:24 +0200 Subject: [PATCH 186/384] Update template --- .github/ISSUE_TEMPLATE/bug_report.md | 18 ++++++++++++++++++ .github/ISSUE_TEMPLATE/config.yml | 4 ++++ .github/ISSUE_TEMPLATE/example_request.md | 20 ++++++++++++++++++++ .github/ISSUE_TEMPLATE/feature_request.md | 20 ++++++++++++++++++++ example/.flutter-plugins-dependencies | 2 +- 5 files changed, 63 insertions(+), 1 deletion(-) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md create mode 100644 .github/ISSUE_TEMPLATE/config.yml create mode 100644 .github/ISSUE_TEMPLATE/example_request.md create mode 100644 .github/ISSUE_TEMPLATE/feature_request.md diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 00000000..60e254a8 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,18 @@ +--- +name: Bug report +about: There is a problem in how provider behaves +title: "" +labels: + - bug + - needs triage +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** + + + +**Expected behavior** +A clear and concise description of what you expected to happen. diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 00000000..214f81a9 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,4 @@ +blank_issues_enabled: false +contact_links: + - name: I have a problem and I need help + url: https://stackoverflow.com/questions/tagged/flutter \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/example_request.md b/.github/ISSUE_TEMPLATE/example_request.md new file mode 100644 index 00000000..03757f03 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/example_request.md @@ -0,0 +1,20 @@ +--- +name: Documentation improvement request +about: >- + Suggest a new example/documentation or ask for clarification about an + existing one. +title: "" +labels: + - documentation + - needs triage +--- + +**Describe what scenario you think is uncovered by the existing examples/articles** +A clear and concise description of the problem that you want explained. + +**Describe why existing examples/articles do not cover this case** +Explain which examples/articles you have seen before making this request, and +why they did not help you with your problem. + +**Additional context** +Add any other context or screenshots about the documentation request here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 00000000..09f28092 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,20 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: "" +labels: + - enhancement + - needs triage +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. diff --git a/example/.flutter-plugins-dependencies b/example/.flutter-plugins-dependencies index 86dddfe7..b50b9ab9 100644 --- a/example/.flutter-plugins-dependencies +++ b/example/.flutter-plugins-dependencies @@ -1 +1 @@ -{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"shared_preferences","path":"/Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/shared_preferences-0.4.3/","dependencies":[]}],"android":[{"name":"shared_preferences","path":"/Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/shared_preferences-0.4.3/","dependencies":[]}],"macos":[],"linux":[],"windows":[],"web":[]},"dependencyGraph":[{"name":"shared_preferences","dependencies":[]}],"date_created":"2020-09-05 14:03:35.263785","version":"1.22.0-2.0.pre.36"} \ No newline at end of file +{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"shared_preferences","path":"/Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/shared_preferences-0.4.3/","dependencies":[]}],"android":[{"name":"shared_preferences","path":"/Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/shared_preferences-0.4.3/","dependencies":[]}],"macos":[],"linux":[],"windows":[],"web":[]},"dependencyGraph":[{"name":"shared_preferences","dependencies":[]}],"date_created":"2020-09-19 14:47:01.003226","version":"1.22.0-10.0.pre.264"} \ No newline at end of file From 4d9409a6a26614deab75d629530956acab5c4f9b Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Sat, 19 Sep 2020 14:47:33 +0200 Subject: [PATCH 187/384] Use github action --- .github/workflows/build.yml | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 .github/workflows/build.yml diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 00000000..a17003ed --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,32 @@ +name: Build + +on: [push, pull_request] + +jobs: + flutter: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + - uses: subosito/flutter-action@v1 + with: + channel: master + + - name: Install dependencies + run: flutter pub get + working-directory: ${{ matrix.package }} + + - name: Check format + run: flutter format --set-exit-if-changed . + working-directory: ${{ matrix.package }} + + - name: Analyze + run: flutter analyze + working-directory: ${{ matrix.package }} + + - name: Run tests + run: flutter test --coverage + working-directory: ${{ matrix.package }} + + - name: Upload coverage to codecov + run: curl -s https://codecov.io/bash | bash From 62199a487340d330c1bc81f0652038d0770a8510 Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Sat, 19 Sep 2020 14:48:27 +0200 Subject: [PATCH 188/384] Use dev channel --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a17003ed..08de93e2 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -10,7 +10,7 @@ jobs: - uses: actions/checkout@v2 - uses: subosito/flutter-action@v1 with: - channel: master + channel: dev - name: Install dependencies run: flutter pub get From c6b7893c1492109558015d5f41127a2f49b13a86 Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Sat, 19 Sep 2020 14:51:50 +0200 Subject: [PATCH 189/384] Remove travis and use Github Action --- .travis.yml | 23 ----------------------- README.md | 2 +- 2 files changed, 1 insertion(+), 24 deletions(-) delete mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index f30382f3..00000000 --- a/.travis.yml +++ /dev/null @@ -1,23 +0,0 @@ -language: bash -os: - - osx -sudo: false -before_script: - - cd .. - - git clone https://github.com/flutter/flutter.git -b stable - - export PATH=$PATH:$PWD/flutter/bin - - export PATH=$PATH:$PWD/flutter/bin/cache/dart-sdk/bin - - flutter doctor - - cd - -script: - # abort on error - - set -e - - flutter packages get - - flutter format --set-exit-if-changed lib example - - flutter analyze lib example - - flutter test --no-pub --coverage - # export coverage - - bash <(curl -s https://codecov.io/bash) -cache: - directories: - - $HOME/.pub-cache diff --git a/README.md b/README.md index 39426f7c..89abee09 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ [English](https://github.com/rrousselGit/flutter_hooks/blob/master/README.md) | [Português](https://github.com/rrousselGit/flutter_hooks/blob/master/resources/translations/pt_br/README.md) -[![Build Status](https://travis-ci.org/rrousselGit/flutter_hooks.svg?branch=master)](https://travis-ci.org/rrousselGit/flutter_hooks) [![codecov](https://codecov.io/gh/rrousselGit/flutter_hooks/branch/master/graph/badge.svg)](https://codecov.io/gh/rrousselGit/flutter_hooks) [![pub package](https://img.shields.io/pub/v/flutter_hooks.svg)](https://pub.dartlang.org/packages/flutter_hooks) [![pub package](https://img.shields.io/badge/Awesome-Flutter-blue.svg?longCache=true&style=flat-square)](https://github.com/Solido/awesome-flutter) +![Build](https://github.com/rrousselGit/flutter_hooks/workflows/Build/badge.svg) [![codecov](https://codecov.io/gh/rrousselGit/flutter_hooks/branch/master/graph/badge.svg)](https://codecov.io/gh/rrousselGit/flutter_hooks) [![pub package](https://img.shields.io/pub/v/flutter_hooks.svg)](https://pub.dartlang.org/packages/flutter_hooks) [![pub package](https://img.shields.io/badge/Awesome-Flutter-blue.svg?longCache=true&style=flat-square)](https://github.com/Solido/awesome-flutter) From 4d372f11c350a08d073a1200ac98c3edfd0e06a6 Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Tue, 22 Sep 2020 11:24:50 +0200 Subject: [PATCH 190/384] Update templates --- .github/ISSUE_TEMPLATE/bug_report.md | 4 +--- .github/ISSUE_TEMPLATE/example_request.md | 4 +--- .github/ISSUE_TEMPLATE/feature_request.md | 4 +--- 3 files changed, 3 insertions(+), 9 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 60e254a8..666296cf 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -2,9 +2,7 @@ name: Bug report about: There is a problem in how provider behaves title: "" -labels: - - bug - - needs triage +labels: bug, needs triage --- **Describe the bug** diff --git a/.github/ISSUE_TEMPLATE/example_request.md b/.github/ISSUE_TEMPLATE/example_request.md index 03757f03..20c7db06 100644 --- a/.github/ISSUE_TEMPLATE/example_request.md +++ b/.github/ISSUE_TEMPLATE/example_request.md @@ -4,9 +4,7 @@ about: >- Suggest a new example/documentation or ask for clarification about an existing one. title: "" -labels: - - documentation - - needs triage +labels: documentation, needs triage --- **Describe what scenario you think is uncovered by the existing examples/articles** diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index 09f28092..b6157e79 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -2,9 +2,7 @@ name: Feature request about: Suggest an idea for this project title: "" -labels: - - enhancement - - needs triage +labels: enhancement, needs triage --- **Is your feature request related to a problem? Please describe.** From 3e245dff02a2ed714a5325b8f08a46e74f2a4b75 Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Tue, 22 Sep 2020 11:27:32 +0200 Subject: [PATCH 191/384] add help template --- .github/ISSUE_TEMPLATE/config.yml | 3 ++- .github/ISSUE_TEMPLATE/help_request.md | 4 ++++ 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 .github/ISSUE_TEMPLATE/help_request.md diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 214f81a9..c5059cc0 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1,4 +1,5 @@ blank_issues_enabled: false contact_links: - name: I have a problem and I need help - url: https://stackoverflow.com/questions/tagged/flutter \ No newline at end of file + url: https://stackoverflow.com/questions/tagged/flutter + about: Please ask and answer questions here. \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/help_request.md b/.github/ISSUE_TEMPLATE/help_request.md new file mode 100644 index 00000000..d3ad5065 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/help_request.md @@ -0,0 +1,4 @@ +--- +name: I have a problem and I need help +about: Please ask and answer questions here. +--- From 73f4967eac1a5c4dba346247cf1a64d58fecf690 Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Tue, 22 Sep 2020 11:30:36 +0200 Subject: [PATCH 192/384] Remove duplicate template --- .github/ISSUE_TEMPLATE/help_request.md | 4 ---- 1 file changed, 4 deletions(-) delete mode 100644 .github/ISSUE_TEMPLATE/help_request.md diff --git a/.github/ISSUE_TEMPLATE/help_request.md b/.github/ISSUE_TEMPLATE/help_request.md deleted file mode 100644 index d3ad5065..00000000 --- a/.github/ISSUE_TEMPLATE/help_request.md +++ /dev/null @@ -1,4 +0,0 @@ ---- -name: I have a problem and I need help -about: Please ask and answer questions here. ---- From 982ff7f35e5e2e557ab2c9109026e6ff71f88c28 Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Tue, 29 Sep 2020 13:55:06 +0200 Subject: [PATCH 193/384] Increase minimum Flutter version closes #188 --- CHANGELOG.md | 9 ++++++++- example/.flutter-plugins-dependencies | 2 +- pubspec.yaml | 1 + 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 77049b9e..39dfb2ca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,11 @@ -## [0.14.0] +## 0.14.1 + +- Increased the minimum version of the Flutter SDK required to match changes on + `useFocusNode` + + The minimum required is now 1.20.0 (the stable channel is at 1.20.4) + +## 0.14.0 - added all `FocusNode` parameters to `useFocusNode` - Fixed a bug where on hot-reload, a `HookWidget` could potentailly not rebuild diff --git a/example/.flutter-plugins-dependencies b/example/.flutter-plugins-dependencies index b50b9ab9..71a02717 100644 --- a/example/.flutter-plugins-dependencies +++ b/example/.flutter-plugins-dependencies @@ -1 +1 @@ -{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"shared_preferences","path":"/Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/shared_preferences-0.4.3/","dependencies":[]}],"android":[{"name":"shared_preferences","path":"/Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/shared_preferences-0.4.3/","dependencies":[]}],"macos":[],"linux":[],"windows":[],"web":[]},"dependencyGraph":[{"name":"shared_preferences","dependencies":[]}],"date_created":"2020-09-19 14:47:01.003226","version":"1.22.0-10.0.pre.264"} \ No newline at end of file +{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"shared_preferences","path":"/Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/shared_preferences-0.4.3/","dependencies":[]}],"android":[{"name":"shared_preferences","path":"/Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/shared_preferences-0.4.3/","dependencies":[]}],"macos":[],"linux":[],"windows":[],"web":[]},"dependencyGraph":[{"name":"shared_preferences","dependencies":[]}],"date_created":"2020-09-29 13:52:58.574198","version":"1.20.4"} \ No newline at end of file diff --git a/pubspec.yaml b/pubspec.yaml index 2c1983b5..b903d9d0 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -5,6 +5,7 @@ version: 0.14.0 environment: sdk: ">=2.7.0 <3.0.0" + flutter: ">= 1.20.0 <2.0.0" dependencies: flutter: From d0fdf607e082ceb93feacb24a11bfb66a1a0c22e Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Tue, 29 Sep 2020 13:55:28 +0200 Subject: [PATCH 194/384] 0.14.1 --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index b903d9d0..d7293a9f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: flutter_hooks description: A flutter implementation of React hooks. It adds a new kind of widget with enhanced code reuse. homepage: https://github.com/rrousselGit/flutter_hooks -version: 0.14.0 +version: 0.14.1 environment: sdk: ">=2.7.0 <3.0.0" From 35ceb0f9cba3b44a91a59d265f7958c7689e241e Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Sat, 3 Oct 2020 03:01:22 +0100 Subject: [PATCH 195/384] Run the CI everyday and on dev+stable --- .github/workflows/build.yml | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 08de93e2..f0331196 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,6 +1,11 @@ name: Build -on: [push, pull_request] +on: + push: + pull_request: + schedule: + # runs the CI everyday at 10AM + - cron: "0 10 * * *" jobs: flutter: @@ -10,7 +15,9 @@ jobs: - uses: actions/checkout@v2 - uses: subosito/flutter-action@v1 with: - channel: dev + channel: + - dev + - stable - name: Install dependencies run: flutter pub get From b9ee471ba82fddb237970d4553022d2cf371843e Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Sat, 3 Oct 2020 03:05:50 +0100 Subject: [PATCH 196/384] Fix CI --- .github/workflows/build.yml | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f0331196..382c2609 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -11,13 +11,17 @@ jobs: flutter: runs-on: ubuntu-latest + strategy: + matrix: + channel: + - dev + - stable + steps: - uses: actions/checkout@v2 - uses: subosito/flutter-action@v1 with: - channel: - - dev - - stable + channel: ${{ matrix.channel }} - name: Install dependencies run: flutter pub get From b3eeeeda0a7b12abca5a21086f0775c7c0813f9e Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Sat, 3 Oct 2020 03:10:39 +0100 Subject: [PATCH 197/384] Fix lints --- example/.flutter-plugins-dependencies | 2 +- test/use_animation_controller_test.dart | 1 + test/use_ticker_provider_test.dart | 5 ++++- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/example/.flutter-plugins-dependencies b/example/.flutter-plugins-dependencies index 71a02717..8dd01827 100644 --- a/example/.flutter-plugins-dependencies +++ b/example/.flutter-plugins-dependencies @@ -1 +1 @@ -{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"shared_preferences","path":"/Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/shared_preferences-0.4.3/","dependencies":[]}],"android":[{"name":"shared_preferences","path":"/Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/shared_preferences-0.4.3/","dependencies":[]}],"macos":[],"linux":[],"windows":[],"web":[]},"dependencyGraph":[{"name":"shared_preferences","dependencies":[]}],"date_created":"2020-09-29 13:52:58.574198","version":"1.20.4"} \ No newline at end of file +{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"shared_preferences","path":"/Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/shared_preferences-0.4.3/","dependencies":[]}],"android":[{"name":"shared_preferences","path":"/Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/shared_preferences-0.4.3/","dependencies":[]}],"macos":[],"linux":[],"windows":[],"web":[]},"dependencyGraph":[{"name":"shared_preferences","dependencies":[]}],"date_created":"2020-10-03 03:09:46.331755","version":"1.23.0-8.0.pre.47"} \ No newline at end of file diff --git a/test/use_animation_controller_test.dart b/test/use_animation_controller_test.dart index bf03feaf..a2623fa9 100644 --- a/test/use_animation_controller_test.dart +++ b/test/use_animation_controller_test.dart @@ -92,6 +92,7 @@ void main() { verifyNoMoreInteractions(provider); // check has a ticker + // ignore: unawaited_futures controller.forward(); expect(controller.duration, const Duration(seconds: 1)); expect(controller.lowerBound, 24); diff --git a/test/use_ticker_provider_test.dart b/test/use_ticker_provider_test.dart index c9548b67..8105519a 100644 --- a/test/use_ticker_provider_test.dart +++ b/test/use_ticker_provider_test.dart @@ -73,9 +73,12 @@ void main() { )); final animationController = AnimationController( - vsync: provider, duration: const Duration(seconds: 1)); + vsync: provider, + duration: const Duration(seconds: 1), + ); try { + // ignore: unawaited_futures animationController.forward(); await tester.pumpWidget(const SizedBox()); From 17c5790d83dd8099927aa73ce3c8a6b30faac322 Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Wed, 21 Oct 2020 02:57:28 +0100 Subject: [PATCH 198/384] Update lints --- all_lint_rules.yaml | 345 ++++++++++++++++++++++-------------------- analysis_options.yaml | 8 +- test/mock.dart | 2 +- 3 files changed, 187 insertions(+), 168 deletions(-) diff --git a/all_lint_rules.yaml b/all_lint_rules.yaml index 238664b5..5b49ce51 100644 --- a/all_lint_rules.yaml +++ b/all_lint_rules.yaml @@ -1,167 +1,180 @@ linter: - rules: - - always_declare_return_types - - always_put_control_body_on_new_line - - always_put_required_named_parameters_first - - always_require_non_null_named_parameters - - always_specify_types - - annotate_overrides - - avoid_annotating_with_dynamic - - avoid_as - - avoid_bool_literals_in_conditional_expressions - - avoid_catches_without_on_clauses - - avoid_catching_errors - - avoid_classes_with_only_static_members - - avoid_double_and_int_checks - - avoid_empty_else - - avoid_equals_and_hash_code_on_mutable_classes - - avoid_escaping_inner_quotes - - avoid_field_initializers_in_const_classes - - avoid_function_literals_in_foreach_calls - - avoid_implementing_value_types - - avoid_init_to_null - - avoid_js_rounded_ints - - avoid_null_checks_in_equality_operators - - avoid_positional_boolean_parameters - - avoid_print - - avoid_private_typedef_functions - - avoid_redundant_argument_values - - avoid_relative_lib_imports - - avoid_renaming_method_parameters - - avoid_return_types_on_setters - - avoid_returning_null - - avoid_returning_null_for_future - - avoid_returning_null_for_void - - avoid_returning_this - - avoid_setters_without_getters - - avoid_shadowing_type_parameters - - avoid_single_cascade_in_expression_statements - - avoid_slow_async_io - - avoid_types_as_parameter_names - - avoid_types_on_closure_parameters - - avoid_unnecessary_containers - - avoid_unused_constructor_parameters - - avoid_void_async - - avoid_web_libraries_in_flutter - - await_only_futures - - camel_case_extensions - - camel_case_types - - cancel_subscriptions - - cascade_invocations - - close_sinks - - comment_references - - constant_identifier_names - - control_flow_in_finally - - curly_braces_in_flow_control_structures - - diagnostic_describe_all_properties - - directives_ordering - - empty_catches - - empty_constructor_bodies - - empty_statements - - file_names - - flutter_style_todos - - hash_and_equals - - implementation_imports - - invariant_booleans - - iterable_contains_unrelated_type - - join_return_with_assignment - - leading_newlines_in_multiline_strings - - library_names - - library_prefixes - - lines_longer_than_80_chars - - list_remove_unrelated_type - - literal_only_boolean_expressions - - missing_whitespace_between_adjacent_strings - - no_adjacent_strings_in_list - - no_duplicate_case_values - - no_logic_in_create_state - - no_runtimeType_toString - - non_constant_identifier_names - - null_closures - - omit_local_variable_types - - one_member_abstracts - - only_throw_errors - - overridden_fields - - package_api_docs - - package_names - - package_prefixed_library_names - - parameter_assignments - - prefer_adjacent_string_concatenation - - prefer_asserts_in_initializer_lists - - prefer_asserts_with_message - - prefer_collection_literals - - prefer_conditional_assignment - - prefer_const_constructors - - prefer_const_constructors_in_immutables - - prefer_const_declarations - - prefer_const_literals_to_create_immutables - - prefer_constructors_over_static_methods - - prefer_contains - - prefer_double_quotes - - prefer_equal_for_default_values - - prefer_expression_function_bodies - - prefer_final_fields - - prefer_final_in_for_each - - prefer_final_locals - - prefer_for_elements_to_map_fromIterable - - prefer_foreach - - prefer_function_declarations_over_variables - - prefer_generic_function_type_aliases - - prefer_if_elements_to_conditional_expressions - - prefer_if_null_operators - - prefer_initializing_formals - - prefer_inlined_adds - - prefer_int_literals - - prefer_interpolation_to_compose_strings - - prefer_is_empty - - prefer_is_not_empty - - prefer_is_not_operator - - prefer_iterable_whereType - - prefer_mixin - - prefer_null_aware_operators - - prefer_relative_imports - - prefer_single_quotes - - prefer_spread_collections - - prefer_typing_uninitialized_variables - - prefer_void_to_null - - provide_deprecation_message - - public_member_api_docs - - recursive_getters - - slash_for_doc_comments - - sort_child_properties_last - - sort_constructors_first - - sort_pub_dependencies - - sort_unnamed_constructors_first - - test_types_in_equals - - throw_in_finally - - type_annotate_public_apis - - type_init_formals - - unawaited_futures - - unnecessary_await_in_return - - unnecessary_brace_in_string_interps - - unnecessary_const - - unnecessary_final - - unnecessary_getters_setters - - unnecessary_lambdas - - unnecessary_new - - unnecessary_null_aware_assignments - - unnecessary_null_in_if_null_operators - - unnecessary_overrides - - unnecessary_parenthesis - - unnecessary_raw_strings - - unnecessary_statements - - unnecessary_string_escapes - - unnecessary_string_interpolations - - unnecessary_this - - unrelated_type_equality_checks - - unsafe_html - - use_full_hex_values_for_flutter_colors - - use_function_type_syntax_for_parameters - - use_key_in_widget_constructors - - use_raw_strings - - use_rethrow_when_possible - - use_setters_to_change_properties - - use_string_buffers - - use_to_and_as_if_applicable - - valid_regexps - - void_checks \ No newline at end of file + rules: + - always_declare_return_types + - always_put_control_body_on_new_line + - always_put_required_named_parameters_first + - always_require_non_null_named_parameters + - always_specify_types + - always_use_package_imports + - annotate_overrides + - avoid_annotating_with_dynamic + - avoid_as + - avoid_bool_literals_in_conditional_expressions + - avoid_catches_without_on_clauses + - avoid_catching_errors + - avoid_classes_with_only_static_members + - avoid_double_and_int_checks + - avoid_empty_else + - avoid_equals_and_hash_code_on_mutable_classes + - avoid_escaping_inner_quotes + - avoid_field_initializers_in_const_classes + - avoid_function_literals_in_foreach_calls + - avoid_implementing_value_types + - avoid_init_to_null + - avoid_js_rounded_ints + - avoid_null_checks_in_equality_operators + - avoid_positional_boolean_parameters + - avoid_print + - avoid_private_typedef_functions + - avoid_redundant_argument_values + - avoid_relative_lib_imports + - avoid_renaming_method_parameters + - avoid_return_types_on_setters + - avoid_returning_null + - avoid_returning_null_for_future + - avoid_returning_null_for_void + - avoid_returning_this + - avoid_setters_without_getters + - avoid_shadowing_type_parameters + - avoid_single_cascade_in_expression_statements + - avoid_slow_async_io + - avoid_type_to_string + - avoid_types_as_parameter_names + - avoid_types_on_closure_parameters + - avoid_unnecessary_containers + - avoid_unused_constructor_parameters + - avoid_void_async + - avoid_web_libraries_in_flutter + - await_only_futures + - camel_case_extensions + - camel_case_types + - cancel_subscriptions + - cascade_invocations + - cast_nullable_to_non_nullable + - close_sinks + - comment_references + - constant_identifier_names + - control_flow_in_finally + - curly_braces_in_flow_control_structures + - diagnostic_describe_all_properties + - directives_ordering + - do_not_use_environment + - empty_catches + - empty_constructor_bodies + - empty_statements + - exhaustive_cases + - file_names + - flutter_style_todos + - hash_and_equals + - implementation_imports + - invariant_booleans + - iterable_contains_unrelated_type + - join_return_with_assignment + - leading_newlines_in_multiline_strings + - library_names + - library_prefixes + - lines_longer_than_80_chars + - list_remove_unrelated_type + - literal_only_boolean_expressions + - missing_whitespace_between_adjacent_strings + - no_adjacent_strings_in_list + - no_default_cases + - no_duplicate_case_values + - no_logic_in_create_state + - no_runtimeType_toString + - non_constant_identifier_names + - null_check_on_nullable_type_parameter + - null_closures + - omit_local_variable_types + - one_member_abstracts + - only_throw_errors + - overridden_fields + - package_api_docs + - package_names + - package_prefixed_library_names + - parameter_assignments + - prefer_adjacent_string_concatenation + - prefer_asserts_in_initializer_lists + - prefer_asserts_with_message + - prefer_collection_literals + - prefer_conditional_assignment + - prefer_const_constructors + - prefer_const_constructors_in_immutables + - prefer_const_declarations + - prefer_const_literals_to_create_immutables + - prefer_constructors_over_static_methods + - prefer_contains + - prefer_double_quotes + - prefer_equal_for_default_values + - prefer_expression_function_bodies + - prefer_final_fields + - prefer_final_in_for_each + - prefer_final_locals + - prefer_for_elements_to_map_fromIterable + - prefer_foreach + - prefer_function_declarations_over_variables + - prefer_generic_function_type_aliases + - prefer_if_elements_to_conditional_expressions + - prefer_if_null_operators + - prefer_initializing_formals + - prefer_inlined_adds + - prefer_int_literals + - prefer_interpolation_to_compose_strings + - prefer_is_empty + - prefer_is_not_empty + - prefer_is_not_operator + - prefer_iterable_whereType + - prefer_mixin + - prefer_null_aware_operators + - prefer_relative_imports + - prefer_single_quotes + - prefer_spread_collections + - prefer_typing_uninitialized_variables + - prefer_void_to_null + - provide_deprecation_message + - public_member_api_docs + - recursive_getters + - sized_box_for_whitespace + - slash_for_doc_comments + - sort_child_properties_last + - sort_constructors_first + - sort_pub_dependencies + - sort_unnamed_constructors_first + - test_types_in_equals + - throw_in_finally + - tighten_type_of_initializing_formals + - type_annotate_public_apis + - type_init_formals + - unawaited_futures + - unnecessary_await_in_return + - unnecessary_brace_in_string_interps + - unnecessary_const + - unnecessary_final + - unnecessary_getters_setters + - unnecessary_lambdas + - unnecessary_new + - unnecessary_null_aware_assignments + - unnecessary_null_checks + - unnecessary_null_in_if_null_operators + - unnecessary_nullable_for_final_variable_declarations + - unnecessary_overrides + - unnecessary_parenthesis + - unnecessary_raw_strings + - unnecessary_statements + - unnecessary_string_escapes + - unnecessary_string_interpolations + - unnecessary_this + - unrelated_type_equality_checks + - unsafe_html + - use_full_hex_values_for_flutter_colors + - use_function_type_syntax_for_parameters + - use_is_even_rather_than_modulo + - use_key_in_widget_constructors + - use_late_for_private_fields_and_variables + - use_raw_strings + - use_rethrow_when_possible + - use_setters_to_change_properties + - use_string_buffers + - use_to_and_as_if_applicable + - valid_regexps + - void_checks \ No newline at end of file diff --git a/analysis_options.yaml b/analysis_options.yaml index 263d685a..85925c0d 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -11,7 +11,7 @@ analyzer: # We explicitly enabled even conflicting rules and are fixing the conflict # in this file included_file_warning: ignore - + # Causes false positives (https://github.com/dart-lang/sdk/issues/41571 top_level_function_literal_block: ignore @@ -62,5 +62,11 @@ linter: # Conflicts with disabling `implicit-dynamic` avoid_annotating_with_dynamic: false + # conflicts with `prefer_relative_imports` + always_use_package_imports: false + + # Disabled for now until we have NNBD as it otherwise conflicts with `missing_return` + no_default_cases: false + # Too verbose diagnostic_describe_all_properties: false \ No newline at end of file diff --git a/test/mock.dart b/test/mock.dart index f128f79d..b9ebda47 100644 --- a/test/mock.dart +++ b/test/mock.dart @@ -6,7 +6,7 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/mockito.dart'; export 'package:flutter_test/flutter_test.dart' - hide Func0, Func1, Func2, Func3, Func4, Func5, Func6; + hide Func0, Func1, Func2, Func3, Func4, Func5, Func6, Fake; export 'package:mockito/mockito.dart'; class HookTest extends Hook { From c0b759455555015455219f49b43032d9f6fcca8c Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Wed, 21 Oct 2020 03:06:39 +0100 Subject: [PATCH 199/384] Sort pub dependencies --- example/.flutter-plugins-dependencies | 2 +- example/pubspec.yaml | 11 +++++------ 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/example/.flutter-plugins-dependencies b/example/.flutter-plugins-dependencies index 8dd01827..384ab649 100644 --- a/example/.flutter-plugins-dependencies +++ b/example/.flutter-plugins-dependencies @@ -1 +1 @@ -{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"shared_preferences","path":"/Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/shared_preferences-0.4.3/","dependencies":[]}],"android":[{"name":"shared_preferences","path":"/Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/shared_preferences-0.4.3/","dependencies":[]}],"macos":[],"linux":[],"windows":[],"web":[]},"dependencyGraph":[{"name":"shared_preferences","dependencies":[]}],"date_created":"2020-10-03 03:09:46.331755","version":"1.23.0-8.0.pre.47"} \ No newline at end of file +{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"shared_preferences","path":"/Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/shared_preferences-0.4.3/","dependencies":[]}],"android":[{"name":"shared_preferences","path":"/Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/shared_preferences-0.4.3/","dependencies":[]}],"macos":[],"linux":[],"windows":[],"web":[]},"dependencyGraph":[{"name":"shared_preferences","dependencies":[]}],"date_created":"2020-10-21 03:06:26.308171","version":"1.24.0-2.0.pre.96"} \ No newline at end of file diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 9e520f93..06717dca 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -7,21 +7,20 @@ environment: sdk: ">=2.7.0 <3.0.0" dependencies: + built_collection: '>=2.0.0 <5.0.0' + built_value: ^6.0.0 flutter: sdk: flutter flutter_hooks: path: ../ - shared_preferences: ^0.4.3 - built_value: ^6.0.0 - built_collection: '>=2.0.0 <5.0.0' provider: 3.1.0+1 + shared_preferences: ^0.4.3 dev_dependencies: + build_runner: ^1.0.0 + built_value_generator: ^6.0.0 flutter_test: sdk: flutter - built_value_generator: ^6.0.0 - build_runner: ^1.0.0 - flutter: uses-material-design: true From 96fd6533ca688938ef79826513291ed6d46f752c Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Wed, 21 Oct 2020 03:12:16 +0100 Subject: [PATCH 200/384] Fix stable CI --- test/mock.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/test/mock.dart b/test/mock.dart index b9ebda47..0bf7fc36 100644 --- a/test/mock.dart +++ b/test/mock.dart @@ -6,6 +6,7 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/mockito.dart'; export 'package:flutter_test/flutter_test.dart' + // ignore: undefined_hidden_name, Fake is only available in master hide Func0, Func1, Func2, Func3, Func4, Func5, Func6, Fake; export 'package:mockito/mockito.dart'; From 17458968044a57fe8d7524bfc68ba91564e4c77b Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Wed, 21 Oct 2020 03:17:47 +0100 Subject: [PATCH 201/384] format --- test/mock.dart | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/test/mock.dart b/test/mock.dart index 0bf7fc36..405d5541 100644 --- a/test/mock.dart +++ b/test/mock.dart @@ -7,7 +7,15 @@ import 'package:mockito/mockito.dart'; export 'package:flutter_test/flutter_test.dart' // ignore: undefined_hidden_name, Fake is only available in master - hide Func0, Func1, Func2, Func3, Func4, Func5, Func6, Fake; + hide + Func0, + Func1, + Func2, + Func3, + Func4, + Func5, + Func6, + Fake; export 'package:mockito/mockito.dart'; class HookTest extends Hook { From 80452c2a48fbde6c4d36ade003ab40312fad08c8 Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Wed, 21 Oct 2020 03:20:37 +0100 Subject: [PATCH 202/384] format --- test/mock.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/mock.dart b/test/mock.dart index 405d5541..d9a22474 100644 --- a/test/mock.dart +++ b/test/mock.dart @@ -6,7 +6,6 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/mockito.dart'; export 'package:flutter_test/flutter_test.dart' - // ignore: undefined_hidden_name, Fake is only available in master hide Func0, Func1, @@ -15,6 +14,7 @@ export 'package:flutter_test/flutter_test.dart' Func4, Func5, Func6, + // ignore: undefined_hidden_name, Fake is only available in master Fake; export 'package:mockito/mockito.dart'; From 5d9f81bc26380dbda076de2fe9f4361a4bf04a25 Mon Sep 17 00:00:00 2001 From: HANAI tohru Date: Sun, 25 Oct 2020 23:09:01 +0900 Subject: [PATCH 203/384] add usePageController based on useScrollController implementation. --- lib/src/hooks.dart | 1 + lib/src/page_controller.dart | 61 ++++++++++++++++++ test/use_page_controller_test.dart | 99 ++++++++++++++++++++++++++++++ 3 files changed, 161 insertions(+) create mode 100644 lib/src/page_controller.dart create mode 100644 test/use_page_controller_test.dart diff --git a/lib/src/hooks.dart b/lib/src/hooks.dart index efb54112..0cade07f 100644 --- a/lib/src/hooks.dart +++ b/lib/src/hooks.dart @@ -16,3 +16,4 @@ part 'tab_controller.dart'; part 'text_controller.dart'; part 'focus.dart'; part 'scroll_controller.dart'; +part 'page_controller.dart'; diff --git a/lib/src/page_controller.dart b/lib/src/page_controller.dart new file mode 100644 index 00000000..f053c313 --- /dev/null +++ b/lib/src/page_controller.dart @@ -0,0 +1,61 @@ +part of 'hooks.dart'; + +/// Creates and disposes a [PageController]. +/// +/// See also: +/// - [PageController] +PageController usePageController({ + int initialPage = 0, + bool keepPage = true, + double viewportFraction = 1.0, + List keys, +}) { + return use( + _PageControllerHook( + initialPage: initialPage, + keepPage: keepPage, + viewportFraction: viewportFraction, + keys: keys, + ), + ); +} + +class _PageControllerHook extends Hook { + const _PageControllerHook({ + this.initialPage, + this.keepPage, + this.viewportFraction, + List keys, + }) : super(keys: keys); + + final int initialPage; + final bool keepPage; + final double viewportFraction; + + @override + HookState> createState() => + _PageControllerHookState(); +} + +class _PageControllerHookState + extends HookState { + PageController controller; + + @override + void initHook() { + controller = PageController( + initialPage: hook.initialPage, + keepPage: hook.keepPage, + viewportFraction: hook.viewportFraction, + ); + } + + @override + PageController build(BuildContext context) => controller; + + @override + void dispose() => controller.dispose(); + + @override + String get debugLabel => 'usePageController'; +} diff --git a/test/use_page_controller_test.dart b/test/use_page_controller_test.dart new file mode 100644 index 00000000..4e514139 --- /dev/null +++ b/test/use_page_controller_test.dart @@ -0,0 +1,99 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/scheduler.dart'; +import 'package:flutter_test/flutter_test.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) { + usePageController(); + return const SizedBox(); + }), + ); + + final element = tester.element(find.byType(HookBuilder)); + + expect( + element + .toDiagnosticsNode(style: DiagnosticsTreeStyle.offstage) + .toStringDeep(), + equalsIgnoringHashCodes( + 'HookBuilder\n' + ' │ usePageController: PageController#00000(no clients)\n' + ' └SizedBox(renderObject: RenderConstrainedBox#00000)\n', + ), + ); + }); + + group('usePageController', () { + testWidgets('initial values matches with real constructor', (tester) async { + PageController controller; + PageController controller2; + + await tester.pumpWidget( + HookBuilder(builder: (context) { + controller2 = PageController(); + controller = usePageController(); + return Container(); + }), + ); + + expect(controller.initialPage, controller2.initialPage); + expect(controller.keepPage, controller2.keepPage); + expect(controller.viewportFraction, controller2.viewportFraction); + }); + testWidgets("returns a PageController that doesn't change", + (tester) async { + PageController controller; + PageController controller2; + + await tester.pumpWidget( + HookBuilder(builder: (context) { + controller = usePageController(); + return Container(); + }), + ); + + expect(controller, isA()); + + await tester.pumpWidget( + HookBuilder(builder: (context) { + controller2 = usePageController(); + return Container(); + }), + ); + + expect(identical(controller, controller2), isTrue); + }); + + testWidgets('passes hook parameters to the PageController', + (tester) async { + PageController controller; + + await tester.pumpWidget( + HookBuilder( + builder: (context) { + controller = usePageController( + initialPage: 42, + keepPage: false, + viewportFraction: 3.4, + ); + + return Container(); + }, + ), + ); + + expect(controller.initialPage, 42); + expect(controller.keepPage, false); + expect(controller.viewportFraction, 3.4); + }); + }); +} + +class TickerProviderMock extends Mock implements TickerProvider {} From 12e83e449425af02c7c39d4fd132bbe4f8b7ae9c Mon Sep 17 00:00:00 2001 From: HANAI tohru Date: Sun, 25 Oct 2020 23:18:11 +0900 Subject: [PATCH 204/384] reformat --- test/use_page_controller_test.dart | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/test/use_page_controller_test.dart b/test/use_page_controller_test.dart index 4e514139..08b2ddd7 100644 --- a/test/use_page_controller_test.dart +++ b/test/use_page_controller_test.dart @@ -1,9 +1,9 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; -import 'package:flutter_test/flutter_test.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'; @@ -47,8 +47,7 @@ void main() { expect(controller.keepPage, controller2.keepPage); expect(controller.viewportFraction, controller2.viewportFraction); }); - testWidgets("returns a PageController that doesn't change", - (tester) async { + testWidgets("returns a PageController that doesn't change", (tester) async { PageController controller; PageController controller2; @@ -71,8 +70,7 @@ void main() { expect(identical(controller, controller2), isTrue); }); - testWidgets('passes hook parameters to the PageController', - (tester) async { + testWidgets('passes hook parameters to the PageController', (tester) async { PageController controller; await tester.pumpWidget( From 353419fb3c7213695a1e02f18ee88b85566102c1 Mon Sep 17 00:00:00 2001 From: HANAI tohru Date: Tue, 27 Oct 2020 00:39:04 +0900 Subject: [PATCH 205/384] remove dead code from test --- test/use_page_controller_test.dart | 3 --- 1 file changed, 3 deletions(-) diff --git a/test/use_page_controller_test.dart b/test/use_page_controller_test.dart index 08b2ddd7..65e9af12 100644 --- a/test/use_page_controller_test.dart +++ b/test/use_page_controller_test.dart @@ -1,6 +1,5 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/scheduler.dart'; import 'package:flutter_hooks/src/framework.dart'; import 'package:flutter_hooks/src/hooks.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -93,5 +92,3 @@ void main() { }); }); } - -class TickerProviderMock extends Mock implements TickerProvider {} From 7370744039e3bc4a25c6c6a669668073f3591e02 Mon Sep 17 00:00:00 2001 From: HANAI tohru Date: Tue, 27 Oct 2020 00:44:29 +0900 Subject: [PATCH 206/384] update README and CHANGELOG --- CHANGELOG.md | 4 ++++ README.md | 1 + 2 files changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 39dfb2ca..7963cb48 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.15.0 + +- Added `usePageController` to create a `PageController` + ## 0.14.1 - Increased the minimum version of the Flutter SDK required to match changes on diff --git a/README.md b/README.md index 89abee09..fc33a9c3 100644 --- a/README.md +++ b/README.md @@ -345,6 +345,7 @@ A series of hooks with no particular theme. | [useFocusNode](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useFocusNode.html) | Create a `FocusNode` | | [useTabController](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useTabController.html) | Creates and disposes a `TabController`. | | [useScrollController](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useScrollController.html) | Creates and disposes a `ScrollController`. | +| [usePageController](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/usePageController.html) | Creates and disposes a `PageController`. | | [useIsMounted](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useIsMounted.html) | An equivalent to `State.mounted` for hooks | ## Contributions From 8064c3faaa5c0427033744f398d362f1c6ba3894 Mon Sep 17 00:00:00 2001 From: HANAI tohru Date: Tue, 27 Oct 2020 01:00:52 +0900 Subject: [PATCH 207/384] add a test to check the PageController is disposed on unmount --- test/use_page_controller_test.dart | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/test/use_page_controller_test.dart b/test/use_page_controller_test.dart index 65e9af12..cf9b3315 100644 --- a/test/use_page_controller_test.dart +++ b/test/use_page_controller_test.dart @@ -90,5 +90,27 @@ void main() { expect(controller.keepPage, false); expect(controller.viewportFraction, 3.4); }); + + testWidgets('disposes the PageController on unmount', (tester) async { + PageController controller; + + await tester.pumpWidget( + HookBuilder( + builder: (context) { + controller = usePageController(); + return Container(); + }, + ), + ); + + // pump another widget so that the old one gets disposed + await tester.pumpWidget(Container()); + + expect( + () => controller.addListener(null), + throwsA(isFlutterError.having( + (e) => e.message, 'message', contains('disposed'))), + ); + }); }); } From 4094f7e210c0d453b20784ae38e39618c01b1a4b Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Mon, 26 Oct 2020 17:43:10 +0100 Subject: [PATCH 208/384] 0.15.0 --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index d7293a9f..db968eee 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: flutter_hooks description: A flutter implementation of React hooks. It adds a new kind of widget with enhanced code reuse. homepage: https://github.com/rrousselGit/flutter_hooks -version: 0.14.1 +version: 0.15.0 environment: sdk: ">=2.7.0 <3.0.0" From d8986a6f57d78050125403dfc6c60aac41508d4f Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Thu, 19 Nov 2020 23:49:35 +0000 Subject: [PATCH 209/384] fix CI --- example/pubspec.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 06717dca..ef4e464c 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -1,13 +1,14 @@ name: flutter_hooks_gallery description: A new Flutter project. +publish_to: none version: 1.0.0+1 environment: sdk: ">=2.7.0 <3.0.0" dependencies: - built_collection: '>=2.0.0 <5.0.0' + built_collection: ">=2.0.0 <5.0.0" built_value: ^6.0.0 flutter: sdk: flutter From 8b20d7846b0b7df3875614adecc4e32edf642780 Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Mon, 25 Jan 2021 07:49:06 +0000 Subject: [PATCH 210/384] Make build badge redirect to github action --- README.md | 2 +- example/.flutter-plugins-dependencies | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index fc33a9c3..276aeb87 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ [English](https://github.com/rrousselGit/flutter_hooks/blob/master/README.md) | [Português](https://github.com/rrousselGit/flutter_hooks/blob/master/resources/translations/pt_br/README.md) -![Build](https://github.com/rrousselGit/flutter_hooks/workflows/Build/badge.svg) [![codecov](https://codecov.io/gh/rrousselGit/flutter_hooks/branch/master/graph/badge.svg)](https://codecov.io/gh/rrousselGit/flutter_hooks) [![pub package](https://img.shields.io/pub/v/flutter_hooks.svg)](https://pub.dartlang.org/packages/flutter_hooks) [![pub package](https://img.shields.io/badge/Awesome-Flutter-blue.svg?longCache=true&style=flat-square)](https://github.com/Solido/awesome-flutter) +[![Build](https://github.com/rrousselGit/flutter_hooks/workflows/Build/badge.svg)](https://github.com/rrousselGit/flutter_hooks/actions?query=workflow%3ABuild) [![codecov](https://codecov.io/gh/rrousselGit/flutter_hooks/branch/master/graph/badge.svg)](https://codecov.io/gh/rrousselGit/flutter_hooks) [![pub package](https://img.shields.io/pub/v/flutter_hooks.svg)](https://pub.dartlang.org/packages/flutter_hooks) [![pub package](https://img.shields.io/badge/Awesome-Flutter-blue.svg?longCache=true&style=flat-square)](https://github.com/Solido/awesome-flutter) diff --git a/example/.flutter-plugins-dependencies b/example/.flutter-plugins-dependencies index 384ab649..3c61c9dc 100644 --- a/example/.flutter-plugins-dependencies +++ b/example/.flutter-plugins-dependencies @@ -1 +1 @@ -{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"shared_preferences","path":"/Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/shared_preferences-0.4.3/","dependencies":[]}],"android":[{"name":"shared_preferences","path":"/Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/shared_preferences-0.4.3/","dependencies":[]}],"macos":[],"linux":[],"windows":[],"web":[]},"dependencyGraph":[{"name":"shared_preferences","dependencies":[]}],"date_created":"2020-10-21 03:06:26.308171","version":"1.24.0-2.0.pre.96"} \ No newline at end of file +{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"shared_preferences","path":"/Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/shared_preferences-0.4.3/","dependencies":[]}],"android":[{"name":"shared_preferences","path":"/Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/shared_preferences-0.4.3/","dependencies":[]}],"macos":[],"linux":[],"windows":[],"web":[]},"dependencyGraph":[{"name":"shared_preferences","dependencies":[]}],"date_created":"2021-01-25 07:47:45.837247","version":"1.26.0-13.0.pre.144"} \ No newline at end of file From 2d9d3052d4d08473e3e8166a8d5472ddaf16af47 Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Mon, 25 Jan 2021 07:52:23 +0000 Subject: [PATCH 211/384] Fix warnings --- example/lib/star_wars/planet_screen.dart | 2 ++ lib/src/framework.dart | 2 +- lib/src/misc.dart | 2 ++ lib/src/primitives.dart | 1 + 4 files changed, 6 insertions(+), 1 deletion(-) diff --git a/example/lib/star_wars/planet_screen.dart b/example/lib/star_wars/planet_screen.dart index 959886a5..edb1fa5c 100644 --- a/example/lib/star_wars/planet_screen.dart +++ b/example/lib/star_wars/planet_screen.dart @@ -100,6 +100,7 @@ class _Error extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.center, children: [ if (errorMsg != null) Text(errorMsg), + // ignore: deprecated_member_use, ElevatedButton is not available in stable yet RaisedButton( color: Colors.redAccent, onPressed: () async { @@ -124,6 +125,7 @@ class _LoadPageButton extends HookWidget { @override Widget build(BuildContext context) { final state = Provider.of(context); + // ignore: deprecated_member_use, ElevatedButton is not available in stable yet return RaisedButton( onPressed: () async { final url = next ? state.planetPage.next : state.planetPage.previous; diff --git a/lib/src/framework.dart b/lib/src/framework.dart index a9fd32c7..fe133bdd 100644 --- a/lib/src/framework.dart +++ b/lib/src/framework.dart @@ -15,7 +15,7 @@ bool debugHotReloadHooksEnabled = true; /// and all calls to [use] must be made unconditionally, always on the same order. /// /// See [Hook] for more explanations. -// ignore: deprecated_member_use_from_same_package +// ignore: deprecated_member_use R use(Hook hook) => Hook.use(hook); /// [Hook] is similar to a [StatelessWidget], but is not associated diff --git a/lib/src/misc.dart b/lib/src/misc.dart index 20aeca1f..752ded9a 100644 --- a/lib/src/misc.dart +++ b/lib/src/misc.dart @@ -216,4 +216,6 @@ class _IsMountedHookState extends HookState { Object get debugValue => _mounted; } +/// Used by [useIsMounted] to allow widgets to determine if the widget is still +/// in the widget tree or not. typedef IsMounted = bool Function(); diff --git a/lib/src/primitives.dart b/lib/src/primitives.dart index fd3c0c2f..b65e3fd5 100644 --- a/lib/src/primitives.dart +++ b/lib/src/primitives.dart @@ -114,6 +114,7 @@ class _ValueChangedHookState } } +/// A function called when the state of a widget is destroyed. typedef Dispose = void Function(); /// Useful for side-effects and optionally canceling them. From 280689538fb1a18df54d0d4c7e28d6234a1ccb35 Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Mon, 25 Jan 2021 07:57:21 +0000 Subject: [PATCH 212/384] fix stable CI --- lib/src/framework.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/framework.dart b/lib/src/framework.dart index fe133bdd..f61730bb 100644 --- a/lib/src/framework.dart +++ b/lib/src/framework.dart @@ -15,7 +15,7 @@ bool debugHotReloadHooksEnabled = true; /// and all calls to [use] must be made unconditionally, always on the same order. /// /// See [Hook] for more explanations. -// ignore: deprecated_member_use +// ignore: deprecated_member_use, deprecated_member_use_from_same_package R use(Hook hook) => Hook.use(hook); /// [Hook] is similar to a [StatelessWidget], but is not associated From bda4dbb41bf036e56f0f2d7594b55bdea91167cd Mon Sep 17 00:00:00 2001 From: Nicolas Schlecker Date: Wed, 27 Jan 2021 12:17:24 +0100 Subject: [PATCH 213/384] Null safety migration (#211) --- .github/workflows/build.yml | 2 +- .gitignore | 4 +- CHANGELOG.md | 4 + README.md | 16 +- analysis_options.yaml | 3 + example/.flutter-plugins-dependencies | 1 - example/.gitignore | 1 + example/lib/star_wars/planet_screen.dart | 16 +- example/lib/use_effect.dart | 3 +- example/lib/use_reducer.dart | 17 +- example/lib/use_stream.dart | 2 +- example/pubspec.yaml | 2 +- lib/src/animation.dart | 60 +++--- lib/src/async.dart | 147 ++++++++------- lib/src/focus.dart | 44 +++-- lib/src/framework.dart | 161 ++++++++-------- lib/src/listenable.dart | 34 ++-- lib/src/misc.dart | 58 +++--- lib/src/page_controller.dart | 25 +-- lib/src/primitives.dart | 96 ++++------ lib/src/scroll_controller.dart | 27 ++- lib/src/tab_controller.dart | 45 +++-- lib/src/text_controller.dart | 36 ++-- pubspec.yaml | 8 +- resources/translations/pt_br/README.md | 52 ++--- test/hook_builder_test.dart | 8 - test/hook_widget_test.dart | 209 +++++++++++---------- test/is_mounted_test.dart | 2 +- test/memoized_test.dart | 31 +-- test/mock.dart | 77 ++++---- test/pre_build_abort_test.dart | 16 +- test/use_animation_controller_test.dart | 35 ++-- test/use_animation_test.dart | 13 +- test/use_context_test.dart | 2 +- test/use_effect_test.dart | 18 +- test/use_focus_node_test.dart | 8 +- test/use_future_test.dart | 87 ++++----- test/use_listenable_test.dart | 13 -- test/use_page_controller_test.dart | 14 +- test/use_reassemble_test.dart | 11 -- test/use_reducer_test.dart | 170 ++++++++++------- test/use_scroll_controller_test.dart | 10 +- test/use_state_test.dart | 14 +- test/use_stream_controller_test.dart | 6 +- test/use_stream_test.dart | 92 +++++---- test/use_tab_controller_test.dart | 23 ++- test/use_text_editing_controller_test.dart | 24 +-- test/use_ticker_provider_test.dart | 8 +- test/use_value_changed_test.dart | 15 +- test/use_value_listenable_test.dart | 15 +- test/use_value_notifier_test.dart | 23 +-- 51 files changed, 850 insertions(+), 958 deletions(-) delete mode 100644 example/.flutter-plugins-dependencies diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 382c2609..41f26784 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -15,7 +15,7 @@ jobs: matrix: channel: - dev - - stable + # - stable steps: - uses: actions/checkout@v2 diff --git a/.gitignore b/.gitignore index 70606c31..9a2e2c11 100644 --- a/.gitignore +++ b/.gitignore @@ -9,4 +9,6 @@ build/ android/ ios/ /coverage -pubspec.lock \ No newline at end of file +pubspec.lock +.vscode/ +.fvm \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 7963cb48..3c5c249f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.16.0-nullsafety.0 + +Migrated flutter_hooks to null-safety (special thanks to @DevNico for the help!) + ## 0.15.0 - Added `usePageController` to create a `PageController` diff --git a/README.md b/README.md index 276aeb87..29d6fefd 100644 --- a/README.md +++ b/README.md @@ -20,16 +20,15 @@ logic of say `initState` or `dispose`. An obvious example is `AnimationControlle class Example extends StatefulWidget { final Duration duration; - const Example({Key key, @required this.duration}) - : assert(duration != null), - super(key: key); + const Example({Key key, required this.duration}) + : super(key: key); @override _ExampleState createState() => _ExampleState(); } class _ExampleState extends State with SingleTickerProviderStateMixin { - AnimationController _controller; + AnimationController? _controller; @override void initState() { @@ -41,13 +40,13 @@ class _ExampleState extends State 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,9 +73,8 @@ This library proposes a third solution: ```dart class Example extends HookWidget { - const Example({Key key, @required this.duration}) - : assert(duration != null), - super(key: key); + const Example({Key key, required this.duration}) + : super(key: key); final Duration duration; diff --git a/analysis_options.yaml b/analysis_options.yaml index 85925c0d..40420f35 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -17,6 +17,9 @@ analyzer: linter: rules: + # Uninitianalized late variables are dangerous + use_late_for_private_fields_and_variables: false + # Personal preference. I don't find it more readable cascade_invocations: false diff --git a/example/.flutter-plugins-dependencies b/example/.flutter-plugins-dependencies deleted file mode 100644 index 3c61c9dc..00000000 --- a/example/.flutter-plugins-dependencies +++ /dev/null @@ -1 +0,0 @@ -{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"shared_preferences","path":"/Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/shared_preferences-0.4.3/","dependencies":[]}],"android":[{"name":"shared_preferences","path":"/Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/shared_preferences-0.4.3/","dependencies":[]}],"macos":[],"linux":[],"windows":[],"web":[]},"dependencyGraph":[{"name":"shared_preferences","dependencies":[]}],"date_created":"2021-01-25 07:47:45.837247","version":"1.26.0-13.0.pre.144"} \ No newline at end of file diff --git a/example/.gitignore b/example/.gitignore index 47e0b4d6..83ebb9e9 100644 --- a/example/.gitignore +++ b/example/.gitignore @@ -23,6 +23,7 @@ **/doc/api/ .dart_tool/ .flutter-plugins +.flutter-plugins-dependencies .packages .pub-cache/ .pub/ diff --git a/example/lib/star_wars/planet_screen.dart b/example/lib/star_wars/planet_screen.dart index edb1fa5c..245c40b9 100644 --- a/example/lib/star_wars/planet_screen.dart +++ b/example/lib/star_wars/planet_screen.dart @@ -20,7 +20,8 @@ class _PlanetHandler { try { final page = await _starWarsApi.getPlanets(url); _store.dispatch(FetchPlanetPageActionSuccess(page)); - } catch (e) { + } catch (e, stack) { + print('errpr $e $stack'); _store.dispatch(FetchPlanetPageActionError('Error loading Planets')); } } @@ -35,9 +36,10 @@ class PlanetScreen extends HookWidget { Widget build(BuildContext context) { final api = useMemoized(() => StarWarsApi()); - final store = useReducer( + final store = useReducer( reducer, initialState: AppState(), + initialAction: null, ); final planetHandler = useMemoized( @@ -100,9 +102,10 @@ class _Error extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.center, children: [ if (errorMsg != null) Text(errorMsg), - // ignore: deprecated_member_use, ElevatedButton is not available in stable yet - RaisedButton( - color: Colors.redAccent, + ElevatedButton( + style: ButtonStyle( + backgroundColor: MaterialStateProperty.all(Colors.redAccent), + ), onPressed: () async { await Provider.of<_PlanetHandler>( context, @@ -125,8 +128,7 @@ class _LoadPageButton extends HookWidget { @override Widget build(BuildContext context) { final state = Provider.of(context); - // ignore: deprecated_member_use, ElevatedButton is not available in stable yet - return RaisedButton( + return ElevatedButton( onPressed: () async { final url = next ? state.planetPage.next : state.planetPage.previous; await Provider.of<_PlanetHandler>(context, listen: false) diff --git a/example/lib/use_effect.dart b/example/lib/use_effect.dart index d15c23c0..1cfebc3c 100644 --- a/example/lib/use_effect.dart +++ b/example/lib/use_effect.dart @@ -29,7 +29,8 @@ class CustomHookExample extends HookWidget { // new value child: HookBuilder( builder: (context) { - final AsyncSnapshot count = useStream(countController.stream); + final AsyncSnapshot count = + useStream(countController.stream, initialData: 0); return !count.hasData ? const CircularProgressIndicator() diff --git a/example/lib/use_reducer.dart b/example/lib/use_reducer.dart index 40d13ce2..205419b4 100644 --- a/example/lib/use_reducer.dart +++ b/example/lib/use_reducer.dart @@ -6,9 +6,10 @@ import 'package:flutter_hooks/flutter_hooks.dart'; /// /// First, instead of a StatefulWidget, use a HookWidget instead! -// Create the State +@immutable class State { - int counter = 0; + const State({this.counter = 0}); + final int counter; } // Create the actions you wish to dispatch to the reducer @@ -23,15 +24,19 @@ class UseReducerExample extends HookWidget { // Create the reducer function that will handle the actions you dispatch State _reducer(State state, IncrementCounter action) { if (action is IncrementCounter) { - state.counter = state.counter + action.counter; + return State(counter: state.counter + action.counter); } return state; } - // Next, invoke the `useReducer` function with the reducer funtion and initial state to create a + // Next, invoke the `useReducer` function with the reducer function and initial state to create a // `_store` variable that contains the current state and dispatch. Whenever the value is // changed, this Widget will be rebuilt! - final _store = useReducer(_reducer, initialState: State()); + final _store = useReducer( + _reducer, + initialState: const State(), + initialAction: null, + ); return Scaffold( appBar: AppBar( @@ -39,7 +44,7 @@ class UseReducerExample extends HookWidget { ), body: Center( // Read the current value from the counter - child: Text('Button tapped ${_store.state.counter.toString()} times'), + child: Text('Button tapped ${_store.state.counter} times'), ), floatingActionButton: FloatingActionButton( // When the button is pressed, dispatch the Action you wish to trigger! This diff --git a/example/lib/use_stream.dart b/example/lib/use_stream.dart index 91c95ab0..8de63732 100644 --- a/example/lib/use_stream.dart +++ b/example/lib/use_stream.dart @@ -31,7 +31,7 @@ class UseStreamExample extends StatelessWidget { // Stream. This triggers a rebuild whenever a new value is emitted. // // Like normal StreamBuilders, it returns the current AsyncSnapshot. - final snapshot = useStream(stream); + final snapshot = useStream(stream, initialData: 0); // Finally, use the data from the Stream to render a text Widget. // If no data is available, fallback to a default value. diff --git a/example/pubspec.yaml b/example/pubspec.yaml index ef4e464c..9f8d0faf 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -14,7 +14,7 @@ dependencies: sdk: flutter flutter_hooks: path: ../ - provider: 3.1.0+1 + provider: ^4.2.0 shared_preferences: ^0.4.3 dev_dependencies: diff --git a/lib/src/animation.dart b/lib/src/animation.dart index 4c9eacd8..42dd59ee 100644 --- a/lib/src/animation.dart +++ b/lib/src/animation.dart @@ -24,7 +24,7 @@ class _UseAnimationStateHook extends _ListenableStateHook { String get debugLabel => 'useAnimation'; @override - Object get debugValue => (hook.listenable as Animation).value; + Object? get debugValue => (hook.listenable as Animation).value; } /// Creates an [AnimationController] automatically disposed. @@ -41,14 +41,14 @@ class _UseAnimationStateHook extends _ListenableStateHook { /// * [AnimationController], the created object. /// * [useAnimation], to listen to the created [AnimationController]. AnimationController useAnimationController({ - Duration duration, - String debugLabel, + Duration? duration, + String? debugLabel, double initialValue = 0, double lowerBound = 0, double upperBound = 1, - TickerProvider vsync, + TickerProvider? vsync, AnimationBehavior animationBehavior = AnimationBehavior.normal, - List keys, + List? keys, }) { vsync ??= useSingleTickerProvider(keys: keys); @@ -70,16 +70,16 @@ class _AnimationControllerHook extends Hook { const _AnimationControllerHook({ this.duration, this.debugLabel, - this.initialValue, - this.lowerBound, - this.upperBound, - this.vsync, - this.animationBehavior, - List keys, + required this.initialValue, + required this.lowerBound, + required this.upperBound, + required this.vsync, + required this.animationBehavior, + List? keys, }) : super(keys: keys); - final Duration duration; - final String debugLabel; + final Duration? duration; + final String? debugLabel; final double initialValue; final double lowerBound; final double upperBound; @@ -99,21 +99,15 @@ class _AnimationControllerHook extends Hook { class _AnimationControllerHookState extends HookState { - AnimationController _animationController; - - @override - void initHook() { - super.initHook(); - _animationController = AnimationController( - vsync: hook.vsync, - duration: hook.duration, - debugLabel: hook.debugLabel, - lowerBound: hook.lowerBound, - upperBound: hook.upperBound, - animationBehavior: hook.animationBehavior, - value: hook.initialValue, - ); - } + late final AnimationController _animationController = AnimationController( + vsync: hook.vsync, + duration: hook.duration, + debugLabel: hook.debugLabel, + lowerBound: hook.lowerBound, + upperBound: hook.upperBound, + animationBehavior: hook.animationBehavior, + value: hook.initialValue, + ); @override void didUpdateHook(_AnimationControllerHook oldHook) { @@ -148,7 +142,7 @@ class _AnimationControllerHookState /// /// See also: /// * [SingleTickerProviderStateMixin] -TickerProvider useSingleTickerProvider({List keys}) { +TickerProvider useSingleTickerProvider({List? keys}) { return use( keys != null ? _SingleTickerProviderHook(keys) @@ -157,7 +151,7 @@ TickerProvider useSingleTickerProvider({List keys}) { } class _SingleTickerProviderHook extends Hook { - const _SingleTickerProviderHook([List keys]) : super(keys: keys); + const _SingleTickerProviderHook([List? keys]) : super(keys: keys); @override _TickerProviderHookState createState() => _TickerProviderHookState(); @@ -166,7 +160,7 @@ class _SingleTickerProviderHook extends Hook { class _TickerProviderHookState extends HookState implements TickerProvider { - Ticker _ticker; + Ticker? _ticker; @override Ticker createTicker(TickerCallback onTick) { @@ -187,7 +181,7 @@ class _TickerProviderHookState @override void dispose() { assert(() { - if (_ticker == null || !_ticker.isActive) { + if (_ticker == null || !_ticker!.isActive) { return true; } throw FlutterError( @@ -201,7 +195,7 @@ class _TickerProviderHookState @override TickerProvider build(BuildContext context) { if (_ticker != null) { - _ticker.muted = !TickerMode.of(context); + _ticker!.muted = !TickerMode.of(context); } return this; } diff --git a/lib/src/async.dart b/lib/src/async.dart index 15d4520a..bdb9974e 100644 --- a/lib/src/async.dart +++ b/lib/src/async.dart @@ -8,16 +8,28 @@ part of 'hooks.dart'; /// See also: /// * [Future], the listened object. /// * [useStream], similar to [useFuture] but for [Stream]. -AsyncSnapshot useFuture(Future future, - {T initialData, bool preserveState = true}) { - return use(_FutureHook(future, - initialData: initialData, preserveState: preserveState)); +AsyncSnapshot useFuture( + Future? future, { + required T initialData, + bool preserveState = true, +}) { + return use( + _FutureHook( + future, + initialData: initialData, + preserveState: preserveState, + ), + ); } class _FutureHook extends Hook> { - const _FutureHook(this.future, {this.initialData, this.preserveState = true}); + const _FutureHook( + this.future, { + required this.initialData, + this.preserveState = true, + }); - final Future future; + final Future? future; final bool preserveState; final T initialData; @@ -29,14 +41,13 @@ class _FutureStateHook extends HookState, _FutureHook> { /// 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; - AsyncSnapshot _snapshot; + Object? _activeCallbackIdentity; + late AsyncSnapshot _snapshot = + AsyncSnapshot.withData(ConnectionState.none, hook.initialData); @override void initHook() { super.initHook(); - _snapshot = - AsyncSnapshot.withData(ConnectionState.none, hook.initialData); _subscribe(); } @@ -66,7 +77,7 @@ class _FutureStateHook extends HookState, _FutureHook> { if (hook.future != null) { final callbackIdentity = Object(); _activeCallbackIdentity = callbackIdentity; - hook.future.then((data) { + hook.future!.then((data) { if (_activeCallbackIdentity == callbackIdentity) { setState(() { _snapshot = AsyncSnapshot.withData(ConnectionState.done, data); @@ -75,7 +86,8 @@ class _FutureStateHook extends HookState, _FutureHook> { }, onError: (dynamic error) { if (_activeCallbackIdentity == callbackIdentity) { setState(() { - _snapshot = AsyncSnapshot.withError(ConnectionState.done, error); + _snapshot = AsyncSnapshot.withError( + ConnectionState.done, error as Object); }); } }); @@ -96,27 +108,39 @@ class _FutureStateHook extends HookState, _FutureHook> { String get debugLabel => 'useFuture'; @override - Object get debugValue => _snapshot; + Object? get debugValue => _snapshot; } /// Subscribes to a [Stream] and return its current state in an [AsyncSnapshot]. /// +/// * [preserveState] defines if the current value should be preserved when changing +/// the [Future] instance. +/// /// See also: /// * [Stream], the object listened. /// * [useFuture], similar to [useStream] but for [Future]. -AsyncSnapshot useStream(Stream stream, - {T initialData, bool preserveState = true}) { - return use(_StreamHook( - stream, - initialData: initialData, - preserveState: preserveState, - )); +AsyncSnapshot useStream( + Stream? stream, { + required T initialData, + bool preserveState = true, +}) { + return use( + _StreamHook( + stream, + initialData: initialData, + preserveState: preserveState, + ), + ); } class _StreamHook extends Hook> { - const _StreamHook(this.stream, {this.initialData, this.preserveState = true}); + const _StreamHook( + this.stream, { + required this.initialData, + required this.preserveState, + }); - final Stream stream; + final Stream? stream; final T initialData; final bool preserveState; @@ -126,13 +150,12 @@ class _StreamHook extends Hook> { /// a clone of [StreamBuilderBase] implementation class _StreamHookState extends HookState, _StreamHook> { - StreamSubscription _subscription; - AsyncSnapshot _summary; + StreamSubscription? _subscription; + late AsyncSnapshot _summary = initial; @override void initHook() { super.initHook(); - _summary = initial(); _subscribe(); } @@ -145,7 +168,7 @@ class _StreamHookState extends HookState, _StreamHook> { if (hook.preserveState) { _summary = afterDisconnected(_summary); } else { - _summary = initial(); + _summary = initial; } } _subscribe(); @@ -159,13 +182,13 @@ class _StreamHookState extends HookState, _StreamHook> { void _subscribe() { if (hook.stream != null) { - _subscription = hook.stream.listen((data) { + _subscription = hook.stream!.listen((data) { setState(() { - _summary = afterData(_summary, data); + _summary = afterData(data); }); }, onError: (dynamic error) { setState(() { - _summary = afterError(_summary, error); + _summary = afterError(error as Object); }); }, onDone: () { setState(() { @@ -177,10 +200,8 @@ class _StreamHookState extends HookState, _StreamHook> { } void _unsubscribe() { - if (_subscription != null) { - _subscription.cancel(); - _subscription = null; - } + _subscription?.cancel(); + _subscription = null; } @override @@ -188,17 +209,17 @@ class _StreamHookState extends HookState, _StreamHook> { return _summary; } - AsyncSnapshot initial() => + AsyncSnapshot get initial => AsyncSnapshot.withData(ConnectionState.none, hook.initialData); AsyncSnapshot afterConnected(AsyncSnapshot current) => current.inState(ConnectionState.waiting); - AsyncSnapshot afterData(AsyncSnapshot current, T data) { + AsyncSnapshot afterData(T data) { return AsyncSnapshot.withData(ConnectionState.active, data); } - AsyncSnapshot afterError(AsyncSnapshot current, Object error) { + AsyncSnapshot afterError(Object error) { return AsyncSnapshot.withError(ConnectionState.active, error); } @@ -217,27 +238,33 @@ class _StreamHookState extends HookState, _StreamHook> { /// See also: /// * [StreamController], the created object /// * [useStream], to listen to the created [StreamController] -StreamController useStreamController( - {bool sync = false, - VoidCallback onListen, - VoidCallback onCancel, - List keys}) { - return use(_StreamControllerHook( - onCancel: onCancel, - onListen: onListen, - sync: sync, - keys: keys, - )); +StreamController useStreamController({ + bool sync = false, + VoidCallback? onListen, + VoidCallback? onCancel, + List? keys, +}) { + return use( + _StreamControllerHook( + onCancel: onCancel, + onListen: onListen, + sync: sync, + keys: keys, + ), + ); } class _StreamControllerHook extends Hook> { - const _StreamControllerHook( - {this.sync = false, this.onListen, this.onCancel, List keys}) - : super(keys: keys); + const _StreamControllerHook({ + required this.sync, + this.onListen, + this.onCancel, + List? keys, + }) : super(keys: keys); final bool sync; - final VoidCallback onListen; - final VoidCallback onCancel; + final VoidCallback? onListen; + final VoidCallback? onCancel; @override _StreamControllerHookState createState() => @@ -246,17 +273,11 @@ class _StreamControllerHook extends Hook> { class _StreamControllerHookState extends HookState, _StreamControllerHook> { - StreamController _controller; - - @override - void initHook() { - super.initHook(); - _controller = StreamController.broadcast( - sync: hook.sync, - onCancel: hook.onCancel, - onListen: hook.onListen, - ); - } + late final _controller = StreamController.broadcast( + sync: hook.sync, + onCancel: hook.onCancel, + onListen: hook.onListen, + ); @override void didUpdateHook(_StreamControllerHook oldHook) { diff --git a/lib/src/focus.dart b/lib/src/focus.dart index 8db92254..fa8b80ae 100644 --- a/lib/src/focus.dart +++ b/lib/src/focus.dart @@ -5,31 +5,34 @@ part of 'hooks.dart'; /// See also: /// - [FocusNode] FocusNode useFocusNode({ - String debugLabel, - FocusOnKeyCallback onKey, + String? debugLabel, + FocusOnKeyCallback? onKey, bool skipTraversal = false, bool canRequestFocus = true, bool descendantsAreFocusable = true, -}) => - use(_FocusNodeHook( +}) { + return use( + _FocusNodeHook( debugLabel: debugLabel, onKey: onKey, skipTraversal: skipTraversal, canRequestFocus: canRequestFocus, descendantsAreFocusable: descendantsAreFocusable, - )); + ), + ); +} class _FocusNodeHook extends Hook { const _FocusNodeHook({ this.debugLabel, this.onKey, - this.skipTraversal, - this.canRequestFocus, - this.descendantsAreFocusable, + required this.skipTraversal, + required this.canRequestFocus, + required this.descendantsAreFocusable, }); - final String debugLabel; - final FocusOnKeyCallback onKey; + final String? debugLabel; + final FocusOnKeyCallback? onKey; final bool skipTraversal; final bool canRequestFocus; final bool descendantsAreFocusable; @@ -41,18 +44,13 @@ class _FocusNodeHook extends Hook { } class _FocusNodeHookState extends HookState { - FocusNode _focusNode; - - @override - void initHook() { - _focusNode = FocusNode( - debugLabel: hook.debugLabel, - onKey: hook.onKey, - skipTraversal: hook.skipTraversal, - canRequestFocus: hook.canRequestFocus, - descendantsAreFocusable: hook.descendantsAreFocusable, - ); - } + late final FocusNode _focusNode = FocusNode( + debugLabel: hook.debugLabel, + onKey: hook.onKey, + skipTraversal: hook.skipTraversal, + canRequestFocus: hook.canRequestFocus, + descendantsAreFocusable: hook.descendantsAreFocusable, + ); @override void didUpdateHook(_FocusNodeHook oldHook) { @@ -67,7 +65,7 @@ class _FocusNodeHookState extends HookState { FocusNode build(BuildContext context) => _focusNode; @override - void dispose() => _focusNode?.dispose(); + void dispose() => _focusNode.dispose(); @override String get debugLabel => 'useFocusNode'; diff --git a/lib/src/framework.dart b/lib/src/framework.dart index f61730bb..96fecfb4 100644 --- a/lib/src/framework.dart +++ b/lib/src/framework.dart @@ -78,16 +78,10 @@ R use(Hook hook) => Hook.use(hook); /// /// class _UsualState extends State /// with SingleTickerProviderStateMixin { -/// AnimationController _controller; -/// -/// @override -/// void initState() { -/// super.initState(); -/// _controller = AnimationController( -/// vsync: this, -/// duration: const Duration(seconds: 1), -/// ); -/// } +/// late final _controller = AnimationController( +/// vsync: this, +/// duration: const Duration(seconds: 1), +/// ); /// /// @override /// void dispose() { @@ -97,9 +91,7 @@ R use(Hook hook) => Hook.use(hook); /// /// @override /// Widget build(BuildContext context) { -/// return Container( -/// -/// ); +/// return Container(); /// } /// } /// ``` @@ -145,7 +137,7 @@ Hooks can only be called from the build method of a widget that mix-in `Hooks`. Hooks should only be called within the build method of a widget. Calling them outside of build method leads to an unstable state and is therefore prohibited. '''); - return HookElement._currentHookElement._use(hook); + return HookElement._currentHookElement!._use(hook); } /// A list of objects that specify if a [HookState] should be reused or a new one should be created. @@ -153,7 +145,7 @@ Calling them outside of build method leads to an unstable state and is therefore /// When a new [Hook] is created, the framework checks if keys matches using [Hook.shouldPreserveState]. /// If they don't, the previously created [HookState] is disposed, and a new one is created /// using [Hook.createState], followed by [HookState.initHook]. - final List keys; + final List? keys; /// The algorithm to determine if a [HookState] should be reused or disposed. /// @@ -206,28 +198,28 @@ Calling them outside of build method leads to an unstable state and is therefore abstract class HookState> with Diagnosticable { /// Equivalent of [State.context] for [HookState] @protected - BuildContext get context => _element; - HookElement _element; + BuildContext get context => _element!; + HookElement? _element; - R _debugLastBuiltValue; + R? _debugLastBuiltValue; /// The value shown in the devtool. /// /// Defaults to the last value returned by [build]. - Object get debugValue => _debugLastBuiltValue; + Object? get debugValue => _debugLastBuiltValue; /// A flag to not show [debugValue] in the devtool, for hooks that returns nothing. bool get debugSkipValue => false; /// A label used by the devtool to show the state of a hook - String get debugLabel => null; + String? get debugLabel => null; /// Whether the devtool description should skip [debugFillProperties] or not. bool get debugHasShortDescription => true; /// Equivalent of [State.widget] for [HookState] - T get hook => _hook; - T _hook; + T get hook => _hook!; + T? _hook; /// Equivalent of [State.initState] for [HookState] @protected @@ -280,21 +272,20 @@ abstract class HookState> with Diagnosticable { /// As opposed to [setState], the rebuild is optional and can be cancelled right /// before `build` is called, by having [shouldRebuild] return false. void markMayNeedRebuild() { - if (_element._isOptionalRebuild != false) { - _element + if (_element!._isOptionalRebuild != false) { + _element! .._isOptionalRebuild = true - .._shouldRebuildQueue ??= LinkedList() .._shouldRebuildQueue.add(_Entry(shouldRebuild)) ..markNeedsBuild(); } - assert(_element.dirty, 'Bad state'); + assert(_element!.dirty, 'Bad state'); } /// Equivalent of [State.setState] for [HookState] @protected void setState(VoidCallback fn) { fn(); - _element + _element! .._isOptionalRebuild = false ..markNeedsBuild(); } @@ -338,7 +329,7 @@ extension on HookElement { void _appendHook(Hook hook) { final result = _createHookState(hook); _currentHookState = _Entry(result); - _hooks.add(_currentHookState); + _hooks.add(_currentHookState!); } void _unmountAllRemainingHooks() { @@ -346,10 +337,10 @@ extension on HookElement { _needDispose ??= LinkedList(); // Mark all hooks >= this one as needing dispose while (_currentHookState != null) { - final previousHookState = _currentHookState; - _currentHookState = _currentHookState.next; + final previousHookState = _currentHookState!; + _currentHookState = _currentHookState!.next; previousHookState.unlink(); - _needDispose.add(previousHookState); + _needDispose!.add(previousHookState); } } } @@ -358,14 +349,14 @@ extension on HookElement { /// An [Element] that uses a [HookWidget] as its configuration. @visibleForTesting mixin HookElement on ComponentElement { - static HookElement _currentHookElement; + static HookElement? _currentHookElement; - _Entry _currentHookState; - final LinkedList<_Entry> _hooks = LinkedList(); - LinkedList<_Entry> _shouldRebuildQueue; - LinkedList<_Entry> _needDispose; - bool _isOptionalRebuild = false; - Widget _buildCache; + _Entry? _currentHookState; + final _hooks = LinkedList<_Entry>(); + final _shouldRebuildQueue = LinkedList<_Entry>(); + LinkedList<_Entry>? _needDispose; + bool? _isOptionalRebuild = false; + Widget? _buildCache; bool _debugIsInitHook = false; bool _debugDidReassemble = false; @@ -373,7 +364,7 @@ mixin HookElement on ComponentElement { /// A read-only list of all hooks available. /// /// In release mode, returns `null`. - List get debugHooks { + List? get debugHooks { if (!kDebugMode) { return null; } @@ -399,10 +390,8 @@ mixin HookElement on ComponentElement { super.reassemble(); _isOptionalRebuild = false; _debugDidReassemble = true; - if (_hooks != null) { - for (final hook in _hooks) { - hook.value.reassemble(); - } + for (final hook in _hooks) { + hook.value.reassemble(); } } @@ -413,10 +402,10 @@ mixin HookElement on ComponentElement { _shouldRebuildQueue.any((cb) => cb.value()); _isOptionalRebuild = null; - _shouldRebuildQueue?.clear(); + _shouldRebuildQueue.clear(); if (!mustRebuild) { - return _buildCache; + return _buildCache!; } if (kDebugMode) { @@ -430,8 +419,9 @@ mixin HookElement on ComponentElement { _isOptionalRebuild = null; _unmountAllRemainingHooks(); HookElement._currentHookElement = null; - if (_needDispose != null && _needDispose.isNotEmpty) { - for (var toDispose = _needDispose.last; + if (_needDispose != null && _needDispose!.isNotEmpty) { + for (_Entry>>? toDispose = + _needDispose!.last; toDispose != null; toDispose = toDispose.previous) { toDispose.value.dispose(); @@ -440,15 +430,15 @@ mixin HookElement on ComponentElement { } } - return _buildCache; + return _buildCache!; } R _use(Hook hook) { /// At the end of the hooks list if (_currentHookState == null) { _appendHook(hook); - } else if (hook.runtimeType != _currentHookState.value.hook.runtimeType) { - final previousHookType = _currentHookState.value.hook.runtimeType; + } else if (hook.runtimeType != _currentHookState!.value.hook.runtimeType) { + final previousHookType = _currentHookState!.value.hook.runtimeType; _unmountAllRemainingHooks(); if (kDebugMode && _debugDidReassemble) { _appendHook(hook); @@ -459,31 +449,31 @@ Type mismatch between hooks: - new hook: ${hook.runtimeType} '''); } - } else if (hook != _currentHookState.value.hook) { - final previousHook = _currentHookState.value.hook; + } else if (hook != _currentHookState!.value.hook) { + final previousHook = _currentHookState!.value.hook; if (Hook.shouldPreserveState(previousHook, hook)) { - _currentHookState.value + _currentHookState!.value .._hook = hook ..didUpdateHook(previousHook); } else { _needDispose ??= LinkedList(); - _needDispose.add(_Entry(_currentHookState.value)); - _currentHookState.value = _createHookState(hook); + _needDispose!.add(_Entry(_currentHookState!.value)); + _currentHookState!.value = _createHookState(hook); } } - final result = _currentHookState.value.build(this) as R; + final result = _currentHookState!.value.build(this) as R; assert(() { - _currentHookState.value._debugLastBuiltValue = result; + _currentHookState!.value._debugLastBuiltValue = result; return true; }(), ''); - _currentHookState = _currentHookState.next; + _currentHookState = _currentHookState!.next; return result; } @override - T dependOnInheritedWidgetOfExactType({ - Object aspect, + T? dependOnInheritedWidgetOfExactType({ + Object? aspect, }) { assert( !_debugIsInitHook, @@ -496,8 +486,10 @@ Type mismatch between hooks: @override void unmount() { super.unmount(); - if (_hooks != null && _hooks.isNotEmpty) { - for (var hook = _hooks.last; hook != null; hook = hook.previous) { + if (_hooks.isNotEmpty) { + for (_Entry>>? hook = _hooks.last; + hook != null; + hook = hook.previous) { try { hook.value.dispose(); } catch (exception, stack) { @@ -518,22 +510,20 @@ Type mismatch between hooks: @override void deactivate() { - if (_hooks != null) { - for (final hook in _hooks) { - try { - hook.value.deactivate(); - } catch (exception, stack) { - FlutterError.reportError( - FlutterErrorDetails( - exception: exception, - stack: stack, - library: 'hooks library', - context: DiagnosticsNode.message( - 'while deactivating ${hook.runtimeType}', - ), + for (final hook in _hooks) { + try { + hook.value.deactivate(); + } catch (exception, stack) { + FlutterError.reportError( + FlutterErrorDetails( + exception: exception, + stack: stack, + library: 'hooks library', + context: DiagnosticsNode.message( + 'while deactivating ${hook.runtimeType}', ), - ); - } + ), + ); } } super.deactivate(); @@ -542,11 +532,11 @@ Type mismatch between hooks: @override void debugFillProperties(DiagnosticPropertiesBuilder properties) { super.debugFillProperties(properties); - for (final hookState in debugHooks) { + for (final hookState in debugHooks!) { if (hookState.debugHasShortDescription) { if (hookState.debugSkipValue) { properties.add( - StringProperty(hookState.debugLabel, '', ifEmpty: ''), + StringProperty(hookState.debugLabel!, '', ifEmpty: ''), ); } else { properties.add( @@ -575,7 +565,7 @@ Type mismatch between hooks: /// [HookWidget] to store mutable data without implementing a [State]. abstract class HookWidget extends StatelessWidget { /// Initializes [key] for subclasses. - const HookWidget({Key key}) : super(key: key); + const HookWidget({Key? key}) : super(key: key); @override _StatelessHookElement createElement() => _StatelessHookElement(this); @@ -593,7 +583,7 @@ class _StatelessHookElement extends StatelessElement with HookElement { /// [HookWidget] to store mutable data without implementing a [State]. abstract class StatefulHookWidget extends StatefulWidget { /// Initializes [key] for subclasses. - const StatefulHookWidget({Key key}) : super(key: key); + const StatefulHookWidget({Key? key}) : super(key: key); @override _StatefulHookElement createElement() => _StatefulHookElement(this); @@ -609,7 +599,7 @@ BuildContext useContext() { HookElement._currentHookElement != null, '`useContext` can only be called from the build method of HookWidget', ); - return HookElement._currentHookElement; + return HookElement._currentHookElement!; } /// A [HookWidget] that defer its `build` to a callback @@ -618,10 +608,9 @@ class HookBuilder extends HookWidget { /// /// The [builder] argument must not be null. const HookBuilder({ - @required this.builder, - Key key, - }) : assert(builder != null, '`builder` cannot be null'), - super(key: key); + required this.builder, + Key? key, + }) : super(key: key); /// The callback used by [HookBuilder] to create a widget. /// diff --git a/lib/src/listenable.dart b/lib/src/listenable.dart index d1a44ca6..fbfd2227 100644 --- a/lib/src/listenable.dart +++ b/lib/src/listenable.dart @@ -24,7 +24,7 @@ class _UseValueListenableStateHook extends _ListenableStateHook { String get debugLabel => 'useValueListenable'; @override - Object get debugValue => (hook.listenable as ValueListenable).value; + Object? get debugValue => (hook.listenable as ValueListenable).value; } /// Subscribes to a [Listenable] and mark the widget as needing build @@ -39,8 +39,7 @@ T useListenable(T listenable) { } class _ListenableHook extends Hook { - const _ListenableHook(this.listenable) - : assert(listenable != null, 'listenable cannot be null'); + const _ListenableHook(this.listenable); final Listenable listenable; @@ -80,7 +79,7 @@ class _ListenableStateHook extends HookState { String get debugLabel => 'useListenable'; @override - Object get debugValue => hook.listenable; + Object? get debugValue => hook.listenable; } /// Creates a [ValueNotifier] automatically disposed. @@ -91,32 +90,29 @@ class _ListenableStateHook extends HookState { /// See also: /// * [ValueNotifier] /// * [useValueListenable] -ValueNotifier useValueNotifier([T intialData, List keys]) { - return use(_ValueNotifierHook( - initialData: intialData, - keys: keys, - )); +ValueNotifier useValueNotifier(T initialData, [List? keys]) { + return use( + _ValueNotifierHook( + initialData: initialData, + keys: keys, + ), + ); } class _ValueNotifierHook extends Hook> { - const _ValueNotifierHook({List keys, this.initialData}) + const _ValueNotifierHook({List? keys, required this.initialData}) : super(keys: keys); final T initialData; @override - _UseValueNotiferHookState createState() => _UseValueNotiferHookState(); + _UseValueNotifierHookState createState() => + _UseValueNotifierHookState(); } -class _UseValueNotiferHookState +class _UseValueNotifierHookState extends HookState, _ValueNotifierHook> { - ValueNotifier notifier; - - @override - void initHook() { - super.initHook(); - notifier = ValueNotifier(hook.initialData); - } + late final notifier = ValueNotifier(hook.initialData); @override ValueNotifier build(BuildContext context) { diff --git a/lib/src/misc.dart b/lib/src/misc.dart index 752ded9a..0e49618d 100644 --- a/lib/src/misc.dart +++ b/lib/src/misc.dart @@ -24,7 +24,7 @@ typedef Reducer = State Function(State state, Action action); /// [useReducer] manages an read only state that can be updated /// by dispatching actions which are interpreted by a [Reducer]. /// -/// [reducer] is immediatly called on first build with [initialAction] +/// [reducer] is immediately called on first build with [initialAction] /// and [initialState] as parameter. /// /// It is possible to change the [reducer] by calling [useReducer] @@ -33,13 +33,13 @@ typedef Reducer = State Function(State state, Action action); /// See also: /// * [Reducer] /// * [Store] -Store useReducer( +Store useReducer( Reducer reducer, { - State initialState, - Action initialAction, + required State initialState, + required Action initialAction, }) { return use( - _ReducerdHook( + _ReducerHook( reducer, initialAction: initialAction, initialState: initialState, @@ -47,37 +47,39 @@ Store useReducer( ); } -class _ReducerdHook extends Hook> { - const _ReducerdHook(this.reducer, {this.initialState, this.initialAction}) - : assert(reducer != null, 'reducer cannot be null'); +class _ReducerHook extends Hook> { + const _ReducerHook( + this.reducer, { + required this.initialState, + required this.initialAction, + }); final Reducer reducer; final State initialState; final Action initialAction; @override - _ReducerdHookState createState() => - _ReducerdHookState(); + _ReducerHookState createState() => + _ReducerHookState(); } -class _ReducerdHookState - extends HookState, _ReducerdHook> +class _ReducerHookState + extends HookState, _ReducerHook> implements Store { @override - State state; + late State state = hook.reducer(hook.initialState, hook.initialAction); @override void initHook() { super.initHook(); - state = hook.reducer(hook.initialState, hook.initialAction); - // TODO support null - assert(state != null, 'reducers cannot return null'); + // ignore: unnecessary_statements, Force the late variable to compute + state; } @override void dispatch(Action action) { final newState = hook.reducer(state, action); - assert(newState != null, 'recuders cannot return null'); + if (state != newState) { setState(() => state = newState); } @@ -92,15 +94,15 @@ class _ReducerdHookState String get debugLabel => 'useReducer'; @override - Object get debugValue => state; + Object? get debugValue => state; } -/// Returns the previous argument called to [usePrevious]. -T usePrevious(T val) { +/// Returns the previous value passed to [usePrevious] (from the previous widget `build`). +T? usePrevious(T val) { return use(_PreviousHook(val)); } -class _PreviousHook extends Hook { +class _PreviousHook extends Hook { const _PreviousHook(this.value); final T value; @@ -109,8 +111,8 @@ class _PreviousHook extends Hook { _PreviousHookState createState() => _PreviousHookState(); } -class _PreviousHookState extends HookState> { - T previous; +class _PreviousHookState extends HookState> { + T? previous; @override void didUpdateHook(_PreviousHook old) { @@ -118,13 +120,13 @@ class _PreviousHookState extends HookState> { } @override - T build(BuildContext context) => previous; + T? build(BuildContext context) => previous; @override String get debugLabel => 'usePrevious'; @override - Object get debugValue => previous; + Object? get debugValue => previous; } /// Runs the callback on every hot reload @@ -141,8 +143,7 @@ void useReassemble(VoidCallback callback) { } class _ReassembleHook extends Hook { - const _ReassembleHook(this.callback) - : assert(callback != null, 'callback cannot be null'); + const _ReassembleHook(this.callback); final VoidCallback callback; @@ -178,7 +179,6 @@ class _ReassembleHookState extends HookState { /// // Do something /// } /// }); -/// return null; /// }, []); /// ``` /// @@ -213,7 +213,7 @@ class _IsMountedHookState extends HookState { String get debugLabel => 'useIsMounted'; @override - Object get debugValue => _mounted; + Object? get debugValue => _mounted; } /// Used by [useIsMounted] to allow widgets to determine if the widget is still diff --git a/lib/src/page_controller.dart b/lib/src/page_controller.dart index f053c313..8e6d1100 100644 --- a/lib/src/page_controller.dart +++ b/lib/src/page_controller.dart @@ -8,7 +8,7 @@ PageController usePageController({ int initialPage = 0, bool keepPage = true, double viewportFraction = 1.0, - List keys, + List? keys, }) { return use( _PageControllerHook( @@ -22,10 +22,10 @@ PageController usePageController({ class _PageControllerHook extends Hook { const _PageControllerHook({ - this.initialPage, - this.keepPage, - this.viewportFraction, - List keys, + required this.initialPage, + required this.keepPage, + required this.viewportFraction, + List? keys, }) : super(keys: keys); final int initialPage; @@ -39,16 +39,11 @@ class _PageControllerHook extends Hook { class _PageControllerHookState extends HookState { - PageController controller; - - @override - void initHook() { - controller = PageController( - initialPage: hook.initialPage, - keepPage: hook.keepPage, - viewportFraction: hook.viewportFraction, - ); - } + late final controller = PageController( + initialPage: hook.initialPage, + keepPage: hook.keepPage, + viewportFraction: hook.viewportFraction, + ); @override PageController build(BuildContext context) => controller; diff --git a/lib/src/primitives.dart b/lib/src/primitives.dart index b65e3fd5..f9cc77b5 100644 --- a/lib/src/primitives.dart +++ b/lib/src/primitives.dart @@ -2,25 +2,27 @@ part of 'hooks.dart'; /// Cache the instance of a complex object. /// -/// [useMemoized] will immediatly call [valueBuilder] on first call and store its result. +/// [useMemoized] will immediately call [valueBuilder] on first call and store its result. /// Later, when [HookWidget] rebuilds, the call to [useMemoized] will return the previously created instance without calling [valueBuilder]. /// /// A later call of [useMemoized] with different [keys] will call [useMemoized] again to create a new instance. -T useMemoized(T Function() valueBuilder, - [List keys = const []]) { - return use(_MemoizedHook( - valueBuilder, - keys: keys, - )); +T useMemoized( + T Function() valueBuilder, [ + List keys = const [], +]) { + return use( + _MemoizedHook( + valueBuilder, + keys: keys, + ), + ); } class _MemoizedHook extends Hook { const _MemoizedHook( this.valueBuilder, { - List keys = const [], - }) : assert(valueBuilder != null, 'valueBuilder cannot be null'), - assert(keys != null, 'keys cannot be null'), - super(keys: keys); + required List keys, + }) : super(keys: keys); final T Function() valueBuilder; @@ -29,13 +31,7 @@ class _MemoizedHook extends Hook { } class _MemoizedHookState extends HookState> { - T value; - - @override - void initHook() { - super.initHook(); - value = hook.valueBuilder(); - } + late final T value = hook.valueBuilder(); @override T build(BuildContext context) { @@ -52,7 +48,7 @@ class _MemoizedHookState extends HookState> { /// [valueChange] will _not_ be called on the first [useValueChanged] call. /// /// [useValueChanged] can also be used to interpolate -/// Whenever [useValueChanged] is called with a diffent [value], calls [valueChange]. +/// Whenever [useValueChanged] is called with a different [value], calls [valueChange]. /// The value returned by [useValueChanged] is the latest returned value of [valueChange] or `null`. /// /// The following example calls [AnimationController.forward] whenever `color` changes @@ -62,21 +58,20 @@ class _MemoizedHookState extends HookState> { /// Color color; /// /// useValueChanged(color, (_, __)) { -/// controller.forward(); +/// controller.forward(); /// }); /// ``` -R useValueChanged( +R? useValueChanged( T value, - R Function(T oldValue, R oldResult) valueChange, + R? Function(T oldValue, R? oldResult) valueChange, ) { return use(_ValueChangedHook(value, valueChange)); } -class _ValueChangedHook extends Hook { - const _ValueChangedHook(this.value, this.valueChanged) - : assert(valueChanged != null, 'valueChanged cannot be null'); +class _ValueChangedHook extends Hook { + const _ValueChangedHook(this.value, this.valueChanged); - final R Function(T oldValue, R oldResult) valueChanged; + final R? Function(T oldValue, R? oldResult) valueChanged; final T value; @override @@ -84,8 +79,8 @@ class _ValueChangedHook extends Hook { } class _ValueChangedHookState - extends HookState> { - R _result; + extends HookState> { + R? _result; @override void didUpdateHook(_ValueChangedHook oldHook) { @@ -96,7 +91,7 @@ class _ValueChangedHookState } @override - R build(BuildContext context) { + R? build(BuildContext context) { return _result; } @@ -130,7 +125,7 @@ typedef Dispose = void Function(); /// In which case, [effect] is called once on the first [useEffect] call and whenever something within [keys] change/ /// /// The following example call [useEffect] to subscribes to a [Stream] and cancel the subscription when the widget is disposed. -/// ALso ifthe [Stream] change, it will cancel the listening on the previous [Stream] and listen to the new one. +/// ALso if the [Stream] change, it will cancel the listening on the previous [Stream] and listen to the new one. /// /// ```dart /// Stream stream; @@ -144,23 +139,21 @@ typedef Dispose = void Function(); /// [stream], /// ); /// ``` -void useEffect(Dispose Function() effect, [List keys]) { +void useEffect(Dispose? Function() effect, [List? keys]) { use(_EffectHook(effect, keys)); } class _EffectHook extends Hook { - const _EffectHook(this.effect, [List keys]) - : assert(effect != null, 'effect cannot be null'), - super(keys: keys); + const _EffectHook(this.effect, [List? keys]) : super(keys: keys); - final Dispose Function() effect; + final Dispose? Function() effect; @override _EffectHookState createState() => _EffectHookState(); } class _EffectHookState extends HookState { - Dispose disposer; + Dispose? disposer; @override void initHook() { @@ -173,9 +166,7 @@ class _EffectHookState extends HookState { super.didUpdateHook(oldHook); if (hook.keys == null) { - if (disposer != null) { - disposer(); - } + disposer?.call(); scheduleEffect(); } } @@ -184,11 +175,7 @@ class _EffectHookState extends HookState { void build(BuildContext context) {} @override - void dispose() { - if (disposer != null) { - disposer(); - } - } + void dispose() => disposer?.call(); void scheduleEffect() { disposer = hook.effect(); @@ -201,7 +188,7 @@ class _EffectHookState extends HookState { bool get debugSkipValue => true; } -/// Create variable and subscribes to it. +/// Creates a variable and subscribes to it. /// /// Whenever [ValueNotifier.value] updates, it will mark the caller [HookWidget] /// as needing build. @@ -229,12 +216,12 @@ class _EffectHookState extends HookState { /// /// * [ValueNotifier] /// * [useStreamController], an alternative to [ValueNotifier] for state. -ValueNotifier useState([T initialData]) { +ValueNotifier useState(T initialData) { return use(_StateHook(initialData: initialData)); } class _StateHook extends Hook> { - const _StateHook({this.initialData}); + const _StateHook({required this.initialData}); final T initialData; @@ -243,13 +230,8 @@ class _StateHook extends Hook> { } class _StateHookState extends HookState, _StateHook> { - ValueNotifier _state; - - @override - void initHook() { - super.initHook(); - _state = ValueNotifier(hook.initialData)..addListener(_listener); - } + late final _state = ValueNotifier(hook.initialData) + ..addListener(_listener); @override void dispose() { @@ -257,16 +239,14 @@ class _StateHookState extends HookState, _StateHook> { } @override - ValueNotifier build(BuildContext context) { - return _state; - } + ValueNotifier build(BuildContext context) => _state; void _listener() { setState(() {}); } @override - Object get debugValue => _state.value; + Object? get debugValue => _state.value; @override String get debugLabel => 'useState<$T>'; diff --git a/lib/src/scroll_controller.dart b/lib/src/scroll_controller.dart index b7e98741..5712654a 100644 --- a/lib/src/scroll_controller.dart +++ b/lib/src/scroll_controller.dart @@ -7,8 +7,8 @@ part of 'hooks.dart'; ScrollController useScrollController({ double initialScrollOffset = 0.0, bool keepScrollOffset = true, - String debugLabel, - List keys, + String? debugLabel, + List? keys, }) { return use( _ScrollControllerHook( @@ -22,15 +22,15 @@ ScrollController useScrollController({ class _ScrollControllerHook extends Hook { const _ScrollControllerHook({ - this.initialScrollOffset, - this.keepScrollOffset, + required this.initialScrollOffset, + required this.keepScrollOffset, this.debugLabel, - List keys, + List? keys, }) : super(keys: keys); final double initialScrollOffset; final bool keepScrollOffset; - final String debugLabel; + final String? debugLabel; @override HookState> createState() => @@ -39,16 +39,11 @@ class _ScrollControllerHook extends Hook { class _ScrollControllerHookState extends HookState { - ScrollController controller; - - @override - void initHook() { - controller = ScrollController( - initialScrollOffset: hook.initialScrollOffset, - keepScrollOffset: hook.keepScrollOffset, - debugLabel: hook.debugLabel, - ); - } + late final controller = ScrollController( + initialScrollOffset: hook.initialScrollOffset, + keepScrollOffset: hook.keepScrollOffset, + debugLabel: hook.debugLabel, + ); @override ScrollController build(BuildContext context) => controller; diff --git a/lib/src/tab_controller.dart b/lib/src/tab_controller.dart index e825dceb..b73af414 100644 --- a/lib/src/tab_controller.dart +++ b/lib/src/tab_controller.dart @@ -5,27 +5,29 @@ part of 'hooks.dart'; /// See also: /// - [TabController] TabController useTabController({ - @required int initialLength, - TickerProvider vsync, + required int initialLength, + TickerProvider? vsync, int initialIndex = 0, - List keys, + List? keys, }) { vsync ??= useSingleTickerProvider(keys: keys); - return use(_TabControllerHook( - vsync: vsync, - length: initialLength, - initialIndex: initialIndex, - keys: keys, - )); + return use( + _TabControllerHook( + vsync: vsync, + length: initialLength, + initialIndex: initialIndex, + keys: keys, + ), + ); } class _TabControllerHook extends Hook { const _TabControllerHook({ - @required this.length, - @required this.vsync, - this.initialIndex = 0, - List keys, + required this.length, + required this.vsync, + required this.initialIndex, + List? keys, }) : super(keys: keys); final int length; @@ -39,22 +41,17 @@ class _TabControllerHook extends Hook { class _TabControllerHookState extends HookState { - TabController controller; - - @override - void initHook() { - controller = TabController( - length: hook.length, - initialIndex: hook.initialIndex, - vsync: hook.vsync, - ); - } + late final controller = TabController( + length: hook.length, + initialIndex: hook.initialIndex, + vsync: hook.vsync, + ); @override TabController build(BuildContext context) => controller; @override - void dispose() => controller?.dispose(); + void dispose() => controller.dispose(); @override String get debugLabel => 'useTabController'; diff --git a/lib/src/text_controller.dart b/lib/src/text_controller.dart index 96181923..259a4214 100644 --- a/lib/src/text_controller.dart +++ b/lib/src/text_controller.dart @@ -7,13 +7,16 @@ class _TextEditingControllerHookCreator { /// /// The [text] parameter can be used to set the initial value of the /// controller. - TextEditingController call({String text, List keys}) { + TextEditingController call({String? text, List? keys}) { return use(_TextEditingControllerHook(text, keys)); } /// Creates a [TextEditingController] from the initial [value] that will /// be disposed automatically. - TextEditingController fromValue(TextEditingValue value, [List keys]) { + TextEditingController fromValue( + TextEditingValue value, [ + List? keys, + ]) { return use(_TextEditingControllerHook.fromValue(value, keys)); } } @@ -26,7 +29,7 @@ class _TextEditingControllerHookCreator { /// final controller = useTextEditingController(text: 'initial text'); /// ``` /// -/// To use a [TextEditingController] with an optional inital value, use +/// To use a [TextEditingController] with an optional initial value, use /// ```dart /// final controller = useTextEditingController /// .fromValue(TextEditingValue.empty); @@ -45,7 +48,6 @@ class _TextEditingControllerHookCreator { /// /// useEffect(() { /// controller.text = update; -/// return null; // we don't need to have a special dispose logic /// }, [update]); /// ``` /// @@ -56,19 +58,18 @@ const useTextEditingController = _TextEditingControllerHookCreator(); class _TextEditingControllerHook extends Hook { const _TextEditingControllerHook( this.initialText, [ - List keys, + List? keys, ]) : initialValue = null, super(keys: keys); const _TextEditingControllerHook.fromValue( - this.initialValue, [ - List keys, + TextEditingValue this.initialValue, [ + List? keys, ]) : initialText = null, - assert(initialValue != null, "initialValue can't be null"), super(keys: keys); - final String initialText; - final TextEditingValue initialValue; + final String? initialText; + final TextEditingValue? initialValue; @override _TextEditingControllerHookState createState() { @@ -78,22 +79,15 @@ class _TextEditingControllerHook extends Hook { class _TextEditingControllerHookState extends HookState { - TextEditingController _controller; - - @override - void initHook() { - if (hook.initialValue != null) { - _controller = TextEditingController.fromValue(hook.initialValue); - } else { - _controller = TextEditingController(text: hook.initialText); - } - } + late final _controller = hook.initialValue != null + ? TextEditingController.fromValue(hook.initialValue) + : TextEditingController(text: hook.initialText); @override TextEditingController build(BuildContext context) => _controller; @override - void dispose() => _controller?.dispose(); + void dispose() => _controller.dispose(); @override String get debugLabel => 'useTextEditingController'; diff --git a/pubspec.yaml b/pubspec.yaml index db968eee..adaaaf23 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,11 +1,11 @@ name: flutter_hooks description: A flutter implementation of React hooks. It adds a new kind of widget with enhanced code reuse. homepage: https://github.com/rrousselGit/flutter_hooks -version: 0.15.0 +version: 0.16.0-nullsafety.0 environment: - sdk: ">=2.7.0 <3.0.0" - flutter: ">= 1.20.0 <2.0.0" + sdk: ">=2.12.0-0 <3.0.0" + flutter: ">=1.20.0 <2.0.0" dependencies: flutter: @@ -14,4 +14,4 @@ dependencies: dev_dependencies: flutter_test: sdk: flutter - mockito: ">=4.0.0 <5.0.0" + mockito: "^5.0.0-nullsafety.4" diff --git a/resources/translations/pt_br/README.md b/resources/translations/pt_br/README.md index c31b3676..1f6af085 100644 --- a/resources/translations/pt_br/README.md +++ b/resources/translations/pt_br/README.md @@ -14,14 +14,14 @@ duplicado. ## Motivação -`StatefulWidget` sofrem de um grande problema: é bem difícil reutilizar a lógica, +`StatefulWidget` sofrem de um grande problema: é bem difícil reutilizar a lógica, por exemplo de um `initState` ou `dispose`. Um exemplo é o `AnimationController`: ```dart class Example extends StatefulWidget { final Duration duration; - const Example({Key key, @required this.duration}) + const Example({Key? key, @required this.duration}) : assert(duration != null), super(key: key); @@ -76,7 +76,7 @@ Essa biblioteca propõe uma terceira solução: ```dart class Example extends HookWidget { - const Example({Key key, @required this.duration}) + const Example({Key? key, @required this.duration}) : assert(duration != null), super(key: key); @@ -300,12 +300,12 @@ Eles são divididos em diferentes tipos: Um conjunto de hooks que interagem com diferentes ciclos de vida de um widget -| Nome | descrição | -| ----------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------- | -| [useEffect](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useEffect.html) | Útil para side-effects e opcionalmente, cancelá-los. | -| [useState](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useState.html) | Cria uma variável e escuta suas mudanças. | -| [useMemoized](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useMemoized.html) | Guarda a instância de um objeto complexo. | -| [useContext](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useContext.html) | Obtém o `BuildContext` do `HookWidget`. | +| Nome | descrição | +| ----------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------ | +| [useEffect](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useEffect.html) | Útil para side-effects e opcionalmente, cancelá-los. | +| [useState](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useState.html) | Cria uma variável e escuta suas mudanças. | +| [useMemoized](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useMemoized.html) | Guarda a instância de um objeto complexo. | +| [useContext](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useContext.html) | Obtém o `BuildContext` do `HookWidget`. | | [useValueChanged](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useValueChanged.html) | Observa o value e chama um callback sempre que o valor muda. | ### Object binding @@ -315,40 +315,40 @@ Eles serão responsáveis por criar/atualizar/descartar o objeto. #### dart:async: -| nome | descrição | -| ------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------- | +| nome | descrição | +| ------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------- | | [useStream](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useStream.html) | Inscreve em uma `Stream` e retorna o estado atual num `AsyncSnapshot`. | -| [useStreamController](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useStreamController.html) | Cria um `StreamController` automaticamente descartado. | +| [useStreamController](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useStreamController.html) | Cria um `StreamController` automaticamente descartado. | | [useFuture](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useFuture.html) | Inscreve em uma `Future` e retorna o estado atual num `AsyncSnapshot`. | #### Animação: -| nome | descrição | -| --------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------- | -| [useSingleTickerProvider](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useSingleTickerProvider.html) | Cria um único `TickerProvider`. | +| nome | descrição | +| --------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------- | +| [useSingleTickerProvider](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useSingleTickerProvider.html) | Cria um único `TickerProvider`. | | [useAnimationController](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useAnimationController.html) | Cria um `AnimationController` automaticamente descartado. | -| [useAnimation](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useAnimation.html) | Inscreve um uma `Animation` e retorna seu valor. | +| [useAnimation](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useAnimation.html) | Inscreve um uma `Animation` e retorna seu valor. | #### Listenable: -| nome | descrição | -| ----------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------- | +| nome | descrição | +| ----------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- | | [useListenable](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useListenable.html) | Inscreve em um `Listenable` e marca o widget para um rebuild quando o listener é chamado. | -| [useValueNotifier](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useValueNotifier.html) | Cria um `ValueNotifier` automaticamente descartado. | -| [useValueListenable](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useValueListenable.html) | Inscreve em um `ValueListenable` e retorna seu valor. | +| [useValueNotifier](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useValueNotifier.html) | Cria um `ValueNotifier` automaticamente descartado. | +| [useValueListenable](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useValueListenable.html) | Inscreve em um `ValueListenable` e retorna seu valor. | #### Misc São vários hooks sem um tema particular. -| nome | descrição | -| ----------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------ | +| nome | descrição | +| ----------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------- | | [useReducer](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useReducer.html) | Uma alternativa `useState` para estados complexos. | | [usePrevious](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/usePrevious.html) | Retorna o argumento anterior chamado [usePrevious]. | -| [useTextEditingController](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useTextEditingController-constant.html) | Cria um `TextEditingController` | -| [useFocusNode](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useFocusNode.html) | Cria um `FocusNode` | -| [useTabController](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useTabController.html) | Cria e descarta um `TabController`. | -| [useScrollController](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useScrollController.html) | Cria e descarta um `ScrollController`. | +| [useTextEditingController](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useTextEditingController-constant.html) | Cria um `TextEditingController` | +| [useFocusNode](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useFocusNode.html) | Cria um `FocusNode` | +| [useTabController](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useTabController.html) | Cria e descarta um `TabController`. | +| [useScrollController](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useScrollController.html) | Cria e descarta um `ScrollController`. | ## Contribuições diff --git a/test/hook_builder_test.dart b/test/hook_builder_test.dart index a4e502e4..4fac1d8b 100644 --- a/test/hook_builder_test.dart +++ b/test/hook_builder_test.dart @@ -13,12 +13,4 @@ void main() { expect(find.text('42'), findsOneWidget); }); - - test('builder required', () { - expect( - // ignore: missing_required_param, prefer_const_constructors - () => HookBuilder(), - throwsAssertionError, - ); - }); } diff --git a/test/hook_widget_test.dart b/test/hook_widget_test.dart index c844d33f..5a9327cd 100644 --- a/test/hook_widget_test.dart +++ b/test/hook_widget_test.dart @@ -20,15 +20,15 @@ class InheritedInitHookState extends HookState { } void main() { - final build = MockBuild(); + final build = MockBuild(); final dispose = MockDispose(); final deactivate = MockDeactivate(); final initHook = MockInitHook(); final didUpdateHook = MockDidUpdateHook(); final reassemble = MockReassemble(); - HookTest createHook() { - return HookTest( + HookTest createHook() { + return HookTest( build: build, dispose: dispose, didUpdateHook: didUpdateHook, @@ -157,7 +157,7 @@ void main() { child: HookBuilder( key: value ? _key2 : _key1, builder: (context) { - use(HookTest(deactivate: deactivate1)); + use(HookTest(deactivate: deactivate1)); return Container(); }, ), @@ -165,7 +165,7 @@ void main() { HookBuilder( key: !value ? _key2 : _key1, builder: (context) { - use(HookTest(deactivate: deactivate2)); + use(HookTest(deactivate: deactivate2)); return Container(); }, ), @@ -202,7 +202,8 @@ void main() { final errorBuilder = ErrorWidget.builder; ErrorWidget.builder = MockErrorBuilder(); - when(ErrorWidget.builder(any)).thenReturn(Container()); + final mockError = MockFlutterErrorDetails(); + when(ErrorWidget.builder(mockError)).thenReturn(Container()); final deactivate = MockDeactivate(); when(deactivate()).thenThrow(42); @@ -213,8 +214,8 @@ void main() { final widget = HookBuilder( key: _key, builder: (context) { - use(HookTest(deactivate: deactivate)); - use(HookTest(deactivate: deactivate2)); + use(HookTest(deactivate: deactivate)); + use(HookTest(deactivate: deactivate2)); return Container(); }, ); @@ -273,7 +274,7 @@ void main() { ); }); testWidgets("release mode don't crash", (tester) async { - ValueNotifier notifier; + late ValueNotifier notifier; debugHotReloadHooksEnabled = false; addTearDown(() => debugHotReloadHooksEnabled = true); @@ -297,8 +298,8 @@ void main() { testWidgets('HookElement exposes an immutable list of hooks', (tester) async { await tester.pumpWidget( HookBuilder(builder: (_) { - use(HookTest()); - use(HookTest()); + use(HookTest()); + use(HookTest()); return Container(); }), ); @@ -306,8 +307,8 @@ void main() { final element = tester.element(find.byType(HookBuilder)) as HookElement; expect(element.debugHooks, [ - isA>(), - isA>(), + isA>(), + isA>(), ]); }); testWidgets( @@ -322,7 +323,7 @@ void main() { await tester.pumpWidget( HookBuilder(builder: (_) { - use(HookTest()); + use(HookTest()); throw 1; }), ); @@ -330,8 +331,8 @@ void main() { await tester.pumpWidget( HookBuilder(builder: (_) { - use(HookTest()); - use(HookTest()); + use(HookTest()); + use(HookTest()); throw 2; }), ); @@ -339,9 +340,9 @@ void main() { await tester.pumpWidget( HookBuilder(builder: (_) { - use(HookTest()); - use(HookTest()); - use(HookTest()); + use(HookTest()); + use(HookTest()); + use(HookTest()); return Container(); }), ); @@ -358,7 +359,7 @@ void main() { await tester.pumpWidget( HookBuilder(builder: (_) { - use(HookTest()); + use(HookTest()); throw 1; }), ); @@ -366,9 +367,9 @@ void main() { await tester.pumpWidget( HookBuilder(builder: (_) { - use(HookTest()); - use(HookTest()); - use(HookTest()); + use(HookTest()); + use(HookTest()); + use(HookTest()); throw 2; }), ); @@ -376,7 +377,7 @@ void main() { }); testWidgets( - "After hot-reload that throws it's still possible to add hooks until one build suceed", + "After hot-reload that throws it's still possible to add hooks until one build succeeds", (tester) async { await tester.pumpWidget( HookBuilder(builder: (_) { @@ -395,14 +396,14 @@ void main() { await tester.pumpWidget( HookBuilder(builder: (_) { - use(HookTest()); + use(HookTest()); return Container(); }), ); }); testWidgets( - 'After hot-reload that throws, hooks are correctly disposed when build suceeeds with less hooks', + 'After hot-reload that throws, hooks are correctly disposed when build succeeds with less hooks', (tester) async { await tester.pumpWidget( HookBuilder(builder: (_) { @@ -438,8 +439,8 @@ void main() { await tester.pumpWidget( HookBuilder(builder: (context) { - use(HookTest(dispose: dispose)); - use(HookTest(dispose: dispose2)); + use(HookTest(dispose: dispose)); + use(HookTest(dispose: dispose2)); return Container(); }), ); @@ -449,8 +450,8 @@ void main() { await tester.pumpWidget( HookBuilder(builder: (context) { - use(HookTest(dispose: dispose, keys: const [])); - use(HookTest(dispose: dispose2)); + use(HookTest(dispose: dispose, keys: const [])); + use(HookTest(dispose: dispose2)); return Container(); }), ); @@ -460,8 +461,8 @@ void main() { await tester.pumpWidget( HookBuilder(builder: (context) { - use(HookTest(dispose: dispose, keys: const [])); - use(HookTest(dispose: dispose2, keys: const [])); + use(HookTest(dispose: dispose, keys: const [])); + use(HookTest(dispose: dispose2, keys: const [])); return Container(); }), ); @@ -470,23 +471,25 @@ void main() { verifyNoMoreInteractions(dispose); }); testWidgets('keys recreate hookstate', (tester) async { - List keys; + List? keys; - final createState = MockCreateState>(); - when(createState()).thenReturn(HookStateTest()); + final createState = + MockCreateState>(HookStateTest()); + // when(createState()).thenReturn(HookStateTest()); + + late HookTest hookTest; Widget $build() { return HookBuilder(builder: (context) { - use( - HookTest( - build: build, - dispose: dispose, - didUpdateHook: didUpdateHook, - initHook: initHook, - keys: keys, - createStateFn: createState, - ), + hookTest = HookTest( + build: build, + dispose: dispose, + didUpdateHook: didUpdateHook, + initHook: initHook, + keys: keys, + createStateFn: createState, ); + use(hookTest); return Container(); }); } @@ -561,8 +564,8 @@ void main() { testWidgets('hook & setState', (tester) async { final setState = MockSetState(); final hook = MyHook(); - HookElement hookContext; - MyHookState state; + late HookElement hookContext; + late MyHookState state; await tester.pumpWidget(HookBuilder( builder: (context) { @@ -583,8 +586,8 @@ void main() { }); testWidgets('life-cycles in order', (tester) async { - int result; - HookTest hook; + late int? result; + late HookTest hook; when(build(any)).thenReturn(42); @@ -600,7 +603,7 @@ void main() { expect(result, 42); verifyInOrder([ initHook(), - build(context), + build(any), ]); verifyNoMoreHookInteration(); @@ -618,7 +621,7 @@ void main() { expect(result, 24); verifyInOrder([ didUpdateHook(previousHook), - build(any), + build(context), ]); verifyNoMoreHookInteration(); @@ -641,7 +644,7 @@ void main() { await tester.pumpWidget( HookBuilder(builder: (context) { use(createHook()); - use(HookTest(dispose: dispose2)); + use(HookTest(dispose: dispose2)); return Container(); }), ); @@ -685,7 +688,7 @@ void main() { verifyInOrder([ build(any), ]); - verifyNever(didUpdateHook(any)); + verifyNever(didUpdateHook(hook)); verifyNever(initHook()); verifyNever(dispose()); }); @@ -693,14 +696,14 @@ void main() { testWidgets('rebuild with different hooks crash', (tester) async { await tester.pumpWidget( HookBuilder(builder: (context) { - use(HookTest()); + use(HookTest()); return Container(); }), ); await tester.pumpWidget( HookBuilder(builder: (context) { - use(HookTest()); + use(HookTest()); return Container(); }), ); @@ -758,12 +761,12 @@ void main() { }), ); - expect(() => use(HookTest()), throwsAssertionError); + expect(() => use(HookTest()), throwsAssertionError); }); testWidgets('hot-reload triggers a build', (tester) async { - int result; - HookTest previousHook; + late int? result; + late HookTest previousHook; when(build(any)).thenReturn(42); @@ -803,12 +806,10 @@ void main() { await tester.pumpWidget( HookBuilder(builder: (context) { use(createHook()); - use( - HookTest( - reassemble: reassemble2, - didUpdateHook: didUpdateHook2, - ), - ); + use(HookTest( + reassemble: reassemble2, + didUpdateHook: didUpdateHook2, + )); return Container(); }), ); @@ -830,7 +831,7 @@ void main() { testWidgets("hot-reload don't reassemble newly added hooks", (tester) async { await tester.pumpWidget( HookBuilder(builder: (context) { - use(HookTest()); + use(HookTest()); return Container(); }), ); @@ -840,7 +841,7 @@ void main() { hotReload(tester); await tester.pumpWidget( HookBuilder(builder: (context) { - use(HookTest()); + use(HookTest()); use(createHook()); return Container(); }), @@ -852,12 +853,12 @@ void main() { testWidgets('hot-reload can add hooks at the end of the list', (tester) async { - HookTest hook1; + late HookTest hook1; final dispose2 = MockDispose(); final initHook2 = MockInitHook(); final didUpdateHook2 = MockDidUpdateHook(); - final build2 = MockBuild(); + final build2 = MockBuild(); await tester.pumpWidget( HookBuilder(builder: (context) { @@ -870,7 +871,7 @@ void main() { verifyInOrder([ initHook(), - build(context), + build(any), ]); verifyZeroInteractions(dispose); verifyZeroInteractions(didUpdateHook); @@ -881,7 +882,7 @@ void main() { HookBuilder(builder: (context) { use(createHook()); use( - HookTest( + HookTest( initHook: initHook2, build: build2, didUpdateHook: didUpdateHook2, @@ -894,7 +895,7 @@ void main() { verifyInOrder([ didUpdateHook(hook1), - build(context), + build(any), initHook2(), build2(context), ]); @@ -909,7 +910,7 @@ void main() { final dispose2 = MockDispose(); final initHook2 = MockInitHook(); final didUpdateHook2 = MockDidUpdateHook(); - final build2 = MockBuild(); + final build2 = MockBuild(); await tester.pumpWidget( HookBuilder(builder: (context) { @@ -922,7 +923,7 @@ void main() { verifyInOrder([ initHook(), - build(context), + build(any), ]); verifyZeroInteractions(dispose); verifyZeroInteractions(didUpdateHook); @@ -931,7 +932,7 @@ void main() { await tester.pumpWidget( HookBuilder(builder: (context) { - use(HookTest( + use(HookTest( initHook: initHook2, build: build2, didUpdateHook: didUpdateHook2, @@ -946,7 +947,7 @@ void main() { initHook2(), build2(context), initHook(), - build(context), + build(any), dispose(), ]); verifyNoMoreInteractions(didUpdateHook); @@ -958,13 +959,13 @@ void main() { final dispose2 = MockDispose(); final initHook2 = MockInitHook(); final didUpdateHook2 = MockDidUpdateHook(); - final build2 = MockBuild(); + final build2 = MockBuild(); await tester.pumpWidget( HookBuilder(builder: (context) { use(createHook()); use( - HookTest( + HookTest( initHook: initHook2, build: build2, didUpdateHook: didUpdateHook2, @@ -978,7 +979,7 @@ void main() { verifyInOrder([ initHook(), - build(context), + build(any), initHook2(), build2(context), ]); @@ -1010,28 +1011,28 @@ void main() { verifyZeroInteractions(didUpdateHook2); }); testWidgets('hot-reload disposes hooks when type change', (tester) async { - HookTest hook1; + late HookTest hook1; final dispose2 = MockDispose(); final initHook2 = MockInitHook(); final didUpdateHook2 = MockDidUpdateHook(); - final build2 = MockBuild(); + final build2 = MockBuild(); final dispose3 = MockDispose(); final initHook3 = MockInitHook(); final didUpdateHook3 = MockDidUpdateHook(); - final build3 = MockBuild(); + final build3 = MockBuild(); final dispose4 = MockDispose(); final initHook4 = MockInitHook(); final didUpdateHook4 = MockDidUpdateHook(); - final build4 = MockBuild(); + final build4 = MockBuild(); await tester.pumpWidget( HookBuilder(builder: (context) { use(hook1 = createHook()); - use(HookTest(dispose: dispose2)); - use(HookTest(dispose: dispose3)); + use(HookTest(dispose: dispose2)); + use(HookTest(dispose: dispose3)); use(HookTest(dispose: dispose4)); return Container(); }), @@ -1039,7 +1040,7 @@ void main() { final context = tester.element(find.byType(HookBuilder)); - // We don't care about datas of the first render + // We don't care about the data from the first render clearInteractions(initHook); clearInteractions(didUpdateHook); clearInteractions(dispose); @@ -1067,21 +1068,21 @@ void main() { use(createHook()); // changed type from HookTest use( - HookTest( + HookTest( initHook: initHook2, build: build2, didUpdateHook: didUpdateHook2, ), ); use( - HookTest( + HookTest( initHook: initHook3, build: build3, didUpdateHook: didUpdateHook3, ), ); use( - HookTest( + HookTest( initHook: initHook4, build: build4, didUpdateHook: didUpdateHook4, @@ -1093,7 +1094,7 @@ void main() { verifyInOrder([ didUpdateHook(hook1), - build(context), + build(any), initHook2(), build2(context), initHook3(), @@ -1112,28 +1113,28 @@ void main() { }); testWidgets('hot-reload disposes hooks when type change', (tester) async { - HookTest hook1; + late HookTest hook1; final dispose2 = MockDispose(); final initHook2 = MockInitHook(); final didUpdateHook2 = MockDidUpdateHook(); - final build2 = MockBuild(); + final build2 = MockBuild(); final dispose3 = MockDispose(); final initHook3 = MockInitHook(); final didUpdateHook3 = MockDidUpdateHook(); - final build3 = MockBuild(); + final build3 = MockBuild(); final dispose4 = MockDispose(); final initHook4 = MockInitHook(); final didUpdateHook4 = MockDidUpdateHook(); - final build4 = MockBuild(); + final build4 = MockBuild(); await tester.pumpWidget( HookBuilder(builder: (context) { use(hook1 = createHook()); - use(HookTest(dispose: dispose2)); - use(HookTest(dispose: dispose3)); + use(HookTest(dispose: dispose2)); + use(HookTest(dispose: dispose3)); use(HookTest(dispose: dispose4)); return Container(); }), @@ -1141,7 +1142,7 @@ void main() { final context = tester.element(find.byType(HookBuilder)); - // We don't care about datas of the first render + // We don't care about the data from the first render clearInteractions(initHook); clearInteractions(didUpdateHook); clearInteractions(dispose); @@ -1167,17 +1168,17 @@ void main() { HookBuilder(builder: (context) { use(createHook()); // changed type from HookTest - use(HookTest( + use(HookTest( initHook: initHook2, build: build2, didUpdateHook: didUpdateHook2, )); - use(HookTest( + use(HookTest( initHook: initHook3, build: build3, didUpdateHook: didUpdateHook3, )); - use(HookTest( + use(HookTest( initHook: initHook4, build: build4, didUpdateHook: didUpdateHook4, @@ -1188,7 +1189,7 @@ void main() { verifyInOrder([ didUpdateHook(hook1), - build(context), + build(any), initHook2(), build2(context), initHook3(), @@ -1274,17 +1275,17 @@ class MyHookState extends HookState { } class MyStatefulHook extends StatefulHookWidget { - const MyStatefulHook({Key key, this.value, this.notifier}) : super(key: key); + const MyStatefulHook({Key? key, this.value, this.notifier}) : super(key: key); - final int value; - final ValueNotifier notifier; + final int? value; + final ValueNotifier? notifier; @override _MyStatefulHookState createState() => _MyStatefulHookState(); } class _MyStatefulHookState extends State { - int value; + int? value; @override void initState() { @@ -1302,7 +1303,7 @@ class _MyStatefulHookState extends State { @override Widget build(BuildContext context) { return Text( - '$value ${useValueListenable(widget.notifier)}', + '$value ${useValueListenable(widget.notifier ?? ValueNotifier(value ?? 42))}', textDirection: TextDirection.ltr, ); } diff --git a/test/is_mounted_test.dart b/test/is_mounted_test.dart index 68670616..70f30270 100644 --- a/test/is_mounted_test.dart +++ b/test/is_mounted_test.dart @@ -5,7 +5,7 @@ import 'package:flutter_test/flutter_test.dart'; void main() { testWidgets('useIsMounted', (tester) async { - IsMounted isMounted; + late IsMounted isMounted; await tester.pumpWidget(HookBuilder( builder: (context) { diff --git a/test/memoized_test.dart b/test/memoized_test.dart index d1ff1acf..0e06a35f 100644 --- a/test/memoized_test.dart +++ b/test/memoized_test.dart @@ -11,28 +11,9 @@ void main() { reset(valueBuilder); }); - testWidgets('invalid parameters', (tester) async { - await tester.pumpWidget( - HookBuilder(builder: (context) { - useMemoized(null); - return Container(); - }), - ); - - expect(tester.takeException(), isAssertionError); - - await tester.pumpWidget( - HookBuilder(builder: (context) { - useMemoized(() {}, null); - return Container(); - }), - ); - expect(tester.takeException(), isAssertionError); - }); - testWidgets('memoized without parameter calls valueBuilder once', (tester) async { - int result; + late int result; when(valueBuilder()).thenReturn(42); @@ -65,7 +46,7 @@ void main() { testWidgets( 'memoized with parameter call valueBuilder again on parameter change', (tester) async { - int result; + late int result; when(valueBuilder()).thenReturn(0); @@ -154,7 +135,7 @@ void main() { }); testWidgets('memoized parameters compared in order', (tester) async { - int result; + late int result; when(valueBuilder()).thenReturn(0); @@ -181,7 +162,7 @@ void main() { verifyNoMoreInteractions(valueBuilder); expect(result, 0); - /* reoder */ + /* reader */ when(valueBuilder()).thenReturn(1); @@ -247,7 +228,7 @@ void main() { testWidgets( "memoized parameter reference do not change don't call valueBuilder", (tester) async { - int result; + late int result; final parameters = []; when(valueBuilder()).thenReturn(0); @@ -308,5 +289,5 @@ void main() { } class MockValueBuilder extends Mock { - int call(); + int call() => super.noSuchMethod(Invocation.getter(#call), 42) as int; } diff --git a/test/mock.dart b/test/mock.dart index d9a22474..8fa8dd7c 100644 --- a/test/mock.dart +++ b/test/mock.dart @@ -18,7 +18,7 @@ export 'package:flutter_test/flutter_test.dart' Fake; export 'package:mockito/mockito.dart'; -class HookTest extends Hook { +class HookTest extends Hook { // ignore: prefer_const_constructors_in_immutables HookTest({ this.build, @@ -29,79 +29,67 @@ class HookTest extends Hook { this.createStateFn, this.didBuild, this.deactivate, - List keys, + List? keys, }) : super(keys: keys); - final R Function(BuildContext context) build; - final void Function() dispose; - final void Function() didBuild; - final void Function() initHook; - final void Function() deactivate; - final void Function(HookTest previousHook) didUpdateHook; - final void Function() reassemble; - final HookStateTest Function() createStateFn; + final R Function(BuildContext context)? build; + final void Function()? dispose; + final void Function()? didBuild; + final void Function()? initHook; + final void Function()? deactivate; + final void Function(HookTest previousHook)? didUpdateHook; + final void Function()? reassemble; + final HookStateTest Function()? createStateFn; @override HookStateTest createState() => - createStateFn != null ? createStateFn() : HookStateTest(); + createStateFn != null ? createStateFn!() : HookStateTest(); } -class HookStateTest extends HookState> { +class HookStateTest extends HookState> { @override void initHook() { super.initHook(); - if (hook.initHook != null) { - hook.initHook(); - } + hook.initHook?.call(); } @override void dispose() { - if (hook.dispose != null) { - hook.dispose(); - } + hook.dispose?.call(); } @override void didUpdateHook(HookTest oldHook) { super.didUpdateHook(oldHook); - if (hook.didUpdateHook != null) { - hook.didUpdateHook(oldHook); - } + hook.didUpdateHook?.call(oldHook); } @override void reassemble() { super.reassemble(); - if (hook.reassemble != null) { - hook.reassemble(); - } + hook.reassemble?.call(); } @override void deactivate() { super.deactivate(); - if (hook.deactivate != null) { - hook.deactivate(); - } + hook.deactivate?.call(); } @override - R build(BuildContext context) { + R? build(BuildContext context) { if (hook.build != null) { - return hook.build(context); + return hook.build!(context); } return null; } } Element _rootOf(Element element) { - Element root; + late Element root; element.visitAncestorElements((e) { - if (e != null) { - root = e; - } - return e != null; + root = e; + return true; }); return root; } @@ -109,7 +97,7 @@ Element _rootOf(Element element) { void hotReload(WidgetTester tester) { final root = _rootOf(tester.allElements.first); - TestWidgetsFlutterBinding.ensureInitialized().buildOwner.reassemble(root); + TestWidgetsFlutterBinding.ensureInitialized().buildOwner?.reassemble(root); } class MockSetState extends Mock { @@ -121,23 +109,32 @@ class MockInitHook extends Mock { } class MockCreateState> extends Mock { - T call(); + MockCreateState(this.value); + final T value; + + T call() => value; } class MockBuild extends Mock { - T call(BuildContext context); + T call(BuildContext? context); } class MockDeactivate extends Mock { void call(); } +class MockFlutterErrorDetails extends Mock implements FlutterErrorDetails { + @override + String toString({DiagnosticLevel? minLevel}) => super.toString(); +} + class MockErrorBuilder extends Mock { - Widget call(FlutterErrorDetails error); + Widget call(FlutterErrorDetails error) => + super.noSuchMethod(Invocation.getter(#call), Container()) as Widget; } class MockOnError extends Mock { - void call(FlutterErrorDetails error); + void call(FlutterErrorDetails? error); } class MockReassemble extends Mock { @@ -145,7 +142,7 @@ class MockReassemble extends Mock { } class MockDidUpdateHook extends Mock { - void call(HookTest hook); + void call(HookTest? hook); } class MockDispose extends Mock { diff --git a/test/pre_build_abort_test.dart b/test/pre_build_abort_test.dart index 0a78f095..68713ede 100644 --- a/test/pre_build_abort_test.dart +++ b/test/pre_build_abort_test.dart @@ -59,7 +59,7 @@ void main() { }); testWidgets('shouldRebuild defaults to true', (tester) async { - MayRebuildState first; + late MayRebuildState first; var buildCount = 0; await tester.pumpWidget( @@ -81,8 +81,8 @@ void main() { testWidgets('can queue multiple mayRebuilds at once', (tester) async { final firstSpy = ShouldRebuildMock(); final secondSpy = ShouldRebuildMock(); - MayRebuildState first; - MayRebuildState second; + late MayRebuildState first; + late MayRebuildState second; var buildCount = 0; await tester.pumpWidget( @@ -220,7 +220,7 @@ void main() { expect(find.text('true'), findsOneWidget); expect(find.text('false'), findsNothing); }); - testWidgets('markMayNeedBuild then didChangeDepencies forces build', + testWidgets('markMayNeedBuild then didChangeDependencies forces build', (tester) async { var buildCount = 0; final notifier = ValueNotifier(0); @@ -271,7 +271,7 @@ class IsPositiveHook extends Hook { class IsPositiveHookState extends HookState { bool dirty = true; - bool value; + late bool value; @override void initHook() { @@ -317,7 +317,7 @@ class IsPositiveHookState extends HookState { class MayRebuild extends Hook { const MayRebuild([this.shouldRebuild]); - final ShouldRebuildMock shouldRebuild; + final ShouldRebuildMock? shouldRebuild; @override MayRebuildState createState() { @@ -331,7 +331,7 @@ class MayRebuildState extends HookState { if (hook.shouldRebuild == null) { return super.shouldRebuild(); } - return hook.shouldRebuild(); + return hook.shouldRebuild!(); } @override @@ -339,5 +339,5 @@ class MayRebuildState extends HookState { } class ShouldRebuildMock extends Mock { - bool call(); + bool call() => super.noSuchMethod(Invocation.getter(#call), false) as bool; } diff --git a/test/use_animation_controller_test.dart b/test/use_animation_controller_test.dart index a2623fa9..6aea37bd 100644 --- a/test/use_animation_controller_test.dart +++ b/test/use_animation_controller_test.dart @@ -1,4 +1,5 @@ import 'package:flutter/foundation.dart'; +import 'package:flutter/scheduler.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; @@ -6,7 +7,7 @@ import 'mock.dart'; void main() { testWidgets('useAnimationController basic', (tester) async { - AnimationController controller; + late AnimationController controller; await tester.pumpWidget( HookBuilder(builder: (context) { @@ -64,13 +65,13 @@ void main() { }); testWidgets('useAnimationController complex', (tester) async { - AnimationController controller; + late AnimationController controller; TickerProvider provider; provider = _TickerProvider(); - when(provider.createTicker(any)).thenAnswer((_) { - return tester - .createTicker(_.positionalArguments[0] as void Function(Duration)); + void onTick(Duration _) {} + when(provider.createTicker(onTick)).thenAnswer((_) { + return tester.createTicker(onTick); }); await tester.pumpWidget( @@ -88,7 +89,7 @@ void main() { }), ); - verify(provider.createTicker(any)).called(1); + verify(provider.createTicker(onTick)).called(1); verifyNoMoreInteractions(provider); // check has a ticker @@ -103,9 +104,8 @@ void main() { final previousController = controller; provider = _TickerProvider(); - when(provider.createTicker(any)).thenAnswer((_) { - return tester - .createTicker(_.positionalArguments[0] as void Function(Duration)); + when(provider.createTicker(onTick)).thenAnswer((_) { + return tester.createTicker(onTick); }); await tester.pumpWidget( @@ -119,7 +119,7 @@ void main() { }), ); - verify(provider.createTicker(any)).called(1); + verify(provider.createTicker(onTick)).called(1); verifyNoMoreInteractions(provider); expect(controller, previousController); expect(controller.duration, const Duration(seconds: 2)); @@ -169,8 +169,8 @@ void main() { }); testWidgets('useAnimationController pass down keys', (tester) async { - List keys; - AnimationController controller; + List? keys; + late AnimationController controller; await tester.pumpWidget(HookBuilder( builder: (context) { controller = useAnimationController(keys: keys); @@ -192,4 +192,13 @@ void main() { }); } -class _TickerProvider extends Mock implements TickerProvider {} +class _TickerProvider extends Mock implements TickerProvider { + @override + Ticker createTicker(TickerCallback onTick) => + super.noSuchMethod(Invocation.getter(#createTicker), Ticker(onTick)) + as Ticker; +} + +class MockEffect extends Mock { + VoidCallback call(); +} diff --git a/test/use_animation_test.dart b/test/use_animation_test.dart index a5ba936f..b71bdc59 100644 --- a/test/use_animation_test.dart +++ b/test/use_animation_test.dart @@ -5,17 +5,6 @@ import 'package:flutter_hooks/flutter_hooks.dart'; import 'mock.dart'; void main() { - testWidgets('useAnimation throws with null', (tester) async { - await tester.pumpWidget(HookBuilder( - builder: (context) { - useAnimation(null); - return Container(); - }, - )); - - expect(tester.takeException(), isAssertionError); - }); - testWidgets('debugFillProperties', (tester) async { await tester.pumpWidget( HookBuilder(builder: (context) { @@ -40,7 +29,7 @@ void main() { testWidgets('useAnimation', (tester) async { var listenable = AnimationController(vsync: tester); - double result; + late double result; Future pump() { return tester.pumpWidget(HookBuilder( diff --git a/test/use_context_test.dart b/test/use_context_test.dart index 8f19bdad..dcae6ad4 100644 --- a/test/use_context_test.dart +++ b/test/use_context_test.dart @@ -7,7 +7,7 @@ import 'mock.dart'; void main() { group('useContext', () { testWidgets('returns current BuildContext during build', (tester) async { - BuildContext res; + late BuildContext res; await tester.pumpWidget(HookBuilder(builder: (context) { res = useContext(); diff --git a/test/use_effect_test.dart b/test/use_effect_test.dart index 29a692ae..66a76f4c 100644 --- a/test/use_effect_test.dart +++ b/test/use_effect_test.dart @@ -7,7 +7,7 @@ import 'mock.dart'; void main() { final effect = MockEffect(); final unrelated = MockWidgetBuild(); - List parameters; + List? parameters; Widget builder() { return HookBuilder(builder: (context) { @@ -47,16 +47,6 @@ void main() { ); }); - testWidgets('useEffect null callback throws', (tester) async { - await tester.pumpWidget( - HookBuilder(builder: (c) { - useEffect(null); - return Container(); - }), - ); - - expect(tester.takeException(), isAssertionError); - }); testWidgets('useEffect calls callback on every build', (tester) async { final effect = MockEffect(); final dispose = MockDispose(); @@ -199,7 +189,7 @@ void main() { ]); verifyNoMoreInteractions(effect); - parameters.add('bar'); + parameters!.add('bar'); await tester.pumpWidget(builder()); verifyNoMoreInteractions(effect); @@ -208,7 +198,7 @@ void main() { testWidgets('useEffect disposer called whenever callback called', (tester) async { final effect = MockEffect(); - List parameters; + List? parameters; Widget builder() { return HookBuilder(builder: (context) { @@ -262,7 +252,7 @@ void main() { } class MockEffect extends Mock { - VoidCallback call(); + VoidCallback? call(); } class MockWidgetBuild extends Mock { diff --git a/test/use_focus_node_test.dart b/test/use_focus_node_test.dart index e939f256..20f3aa7d 100644 --- a/test/use_focus_node_test.dart +++ b/test/use_focus_node_test.dart @@ -6,7 +6,7 @@ import 'mock.dart'; void main() { testWidgets('creates a focus node and disposes it', (tester) async { - FocusNode focusNode; + late FocusNode focusNode; await tester.pumpWidget( HookBuilder(builder: (_) { focusNode = useFocusNode(); @@ -65,7 +65,7 @@ void main() { testWidgets('default values matches with FocusNode', (tester) async { final official = FocusNode(); - FocusNode focusNode; + late FocusNode focusNode; await tester.pumpWidget( HookBuilder(builder: (_) { focusNode = useFocusNode(); @@ -83,7 +83,7 @@ void main() { testWidgets('has all the FocusNode parameters', (tester) async { bool onKey(FocusNode node, RawKeyEvent event) => true; - FocusNode focusNode; + late FocusNode focusNode; await tester.pumpWidget( HookBuilder(builder: (_) { focusNode = useFocusNode( @@ -108,7 +108,7 @@ void main() { bool onKey(FocusNode node, RawKeyEvent event) => true; bool onKey2(FocusNode node, RawKeyEvent event) => true; - FocusNode focusNode; + late FocusNode focusNode; await tester.pumpWidget( HookBuilder(builder: (_) { focusNode = useFocusNode( diff --git a/test/use_future_test.dart b/test/use_future_test.dart index 60e5bdc8..b1f45969 100644 --- a/test/use_future_test.dart +++ b/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 { - AsyncSnapshot value; - Widget Function(BuildContext) builder(Future stream) { + late AsyncSnapshot value; + Widget Function(BuildContext) builder(Future stream) { return (context) { - value = useFuture(stream); + value = useFuture(stream, initialData: null); return Container(); }; } @@ -35,7 +35,7 @@ void main() { await tester.pumpWidget( HookBuilder(builder: (context) { - useFuture(future); + useFuture(future, initialData: 42); return const SizedBox(); }), ); @@ -50,7 +50,8 @@ void main() { .toStringDeep(), equalsIgnoringHashCodes( 'HookBuilder\n' - ' │ useFuture: AsyncSnapshot(ConnectionState.done, 42, null)\n' + ' │ useFuture: AsyncSnapshot(ConnectionState.done, 42, null,\n' + ' │ null)\n' ' └SizedBox(renderObject: RenderConstrainedBox#00000)\n', ), ); @@ -58,10 +59,10 @@ void main() { testWidgets('If preserveState == false, changing future resets value', (tester) async { - AsyncSnapshot value; - Widget Function(BuildContext) builder(Future stream) { + late AsyncSnapshot value; + Widget Function(BuildContext) builder(Future stream) { return (context) { - value = useFuture(stream, preserveState: false); + value = useFuture(stream, initialData: null, preserveState: false); return Container(); }; } @@ -79,57 +80,35 @@ void main() { expect(value.data, 42); }); - Widget Function(BuildContext) snapshotText(Future stream, - {String initialData}) { + Widget Function(BuildContext) snapshotText(Future stream, + {String? initialData}) { return (context) { final snapshot = useFuture(stream, initialData: initialData); return Text(snapshot.toString(), textDirection: TextDirection.ltr); }; } - testWidgets('gracefully handles transition from null future', (tester) async { - await tester.pumpWidget(HookBuilder(builder: snapshotText(null))); - expect(find.text('AsyncSnapshot(ConnectionState.none, null, null)'), - findsOneWidget); - final completer = Completer(); - await tester - .pumpWidget(HookBuilder(builder: snapshotText(completer.future))); - expect( - find.text('AsyncSnapshot(ConnectionState.waiting, null, null)'), - findsOneWidget); - }); - testWidgets('gracefully handles transition to null future', (tester) async { - final completer = Completer(); - await tester - .pumpWidget(HookBuilder(builder: snapshotText(completer.future))); - expect( - find.text('AsyncSnapshot(ConnectionState.waiting, null, null)'), - findsOneWidget); - await tester.pumpWidget(HookBuilder(builder: snapshotText(null))); - expect(find.text('AsyncSnapshot(ConnectionState.none, null, null)'), - findsOneWidget); - completer.complete('hello'); - await eventFiring(tester); - expect(find.text('AsyncSnapshot(ConnectionState.none, null, null)'), - findsOneWidget); - }); testWidgets('gracefully handles transition to other future', (tester) async { final completerA = Completer(); final completerB = Completer(); await tester .pumpWidget(HookBuilder(builder: snapshotText(completerA.future))); expect( - find.text('AsyncSnapshot(ConnectionState.waiting, null, null)'), + find.text( + 'AsyncSnapshot(ConnectionState.waiting, null, null, null)'), findsOneWidget); await tester .pumpWidget(HookBuilder(builder: snapshotText(completerB.future))); expect( - find.text('AsyncSnapshot(ConnectionState.waiting, null, null)'), + find.text( + 'AsyncSnapshot(ConnectionState.waiting, null, null, null)'), findsOneWidget); completerB.complete('B'); completerA.complete('A'); await eventFiring(tester); - expect(find.text('AsyncSnapshot(ConnectionState.done, B, null)'), + expect( + find.text( + 'AsyncSnapshot(ConnectionState.done, B, null, null)'), findsOneWidget); }); testWidgets('tracks life-cycle of Future to success', (tester) async { @@ -137,12 +116,14 @@ void main() { await tester .pumpWidget(HookBuilder(builder: snapshotText(completer.future))); expect( - find.text('AsyncSnapshot(ConnectionState.waiting, null, null)'), + find.text( + 'AsyncSnapshot(ConnectionState.waiting, null, null, null)'), findsOneWidget); completer.complete('hello'); await eventFiring(tester); expect( - find.text('AsyncSnapshot(ConnectionState.done, hello, null)'), + find.text( + 'AsyncSnapshot(ConnectionState.done, hello, null, null)'), findsOneWidget); }); testWidgets('tracks life-cycle of Future to error', (tester) async { @@ -150,40 +131,48 @@ void main() { await tester .pumpWidget(HookBuilder(builder: snapshotText(completer.future))); expect( - find.text('AsyncSnapshot(ConnectionState.waiting, null, null)'), + find.text( + 'AsyncSnapshot(ConnectionState.waiting, null, null, null)'), findsOneWidget); completer.completeError('bad'); await eventFiring(tester); - expect(find.text('AsyncSnapshot(ConnectionState.done, null, bad)'), + expect( + find.text('AsyncSnapshot(ConnectionState.done, null, bad, )'), findsOneWidget); }); testWidgets('runs the builder using given initial data', (tester) async { await tester.pumpWidget(HookBuilder( builder: snapshotText( - null, + Future.value(), initialData: 'I', ), )); - expect(find.text('AsyncSnapshot(ConnectionState.none, I, null)'), + expect( + find.text( + 'AsyncSnapshot(ConnectionState.waiting, I, null, null)'), findsOneWidget); }); testWidgets('ignores initialData when reconfiguring', (tester) async { await tester.pumpWidget(HookBuilder( builder: snapshotText( - null, + Future.value(), initialData: 'I', ), )); - expect(find.text('AsyncSnapshot(ConnectionState.none, I, null)'), + expect( + find.text( + 'AsyncSnapshot(ConnectionState.waiting, I, null, null)'), findsOneWidget); - final completer = Completer(); + final completer = Completer(); await tester.pumpWidget(HookBuilder( builder: snapshotText( completer.future, initialData: 'Ignored', ), )); - expect(find.text('AsyncSnapshot(ConnectionState.waiting, I, null)'), + expect( + find.text( + 'AsyncSnapshot(ConnectionState.waiting, null, null, null)'), findsOneWidget); }); } diff --git a/test/use_listenable_test.dart b/test/use_listenable_test.dart index 2439d716..88a0616d 100644 --- a/test/use_listenable_test.dart +++ b/test/use_listenable_test.dart @@ -5,19 +5,6 @@ import 'package:flutter_hooks/flutter_hooks.dart'; import 'mock.dart'; void main() { - testWidgets('useListenable throws with null', (tester) async { - await tester.pumpWidget( - HookBuilder( - builder: (context) { - useListenable(null); - return Container(); - }, - ), - ); - - expect(tester.takeException(), isAssertionError); - }); - testWidgets('debugFillProperties', (tester) async { await tester.pumpWidget( HookBuilder(builder: (context) { diff --git a/test/use_page_controller_test.dart b/test/use_page_controller_test.dart index cf9b3315..da4def3a 100644 --- a/test/use_page_controller_test.dart +++ b/test/use_page_controller_test.dart @@ -31,8 +31,8 @@ void main() { group('usePageController', () { testWidgets('initial values matches with real constructor', (tester) async { - PageController controller; - PageController controller2; + late PageController controller; + late PageController controller2; await tester.pumpWidget( HookBuilder(builder: (context) { @@ -47,8 +47,8 @@ void main() { expect(controller.viewportFraction, controller2.viewportFraction); }); testWidgets("returns a PageController that doesn't change", (tester) async { - PageController controller; - PageController controller2; + late PageController controller; + late PageController controller2; await tester.pumpWidget( HookBuilder(builder: (context) { @@ -70,7 +70,7 @@ void main() { }); testWidgets('passes hook parameters to the PageController', (tester) async { - PageController controller; + late PageController controller; await tester.pumpWidget( HookBuilder( @@ -92,7 +92,7 @@ void main() { }); testWidgets('disposes the PageController on unmount', (tester) async { - PageController controller; + late PageController controller; await tester.pumpWidget( HookBuilder( @@ -107,7 +107,7 @@ void main() { await tester.pumpWidget(Container()); expect( - () => controller.addListener(null), + () => controller.addListener(() {}), throwsA(isFlutterError.having( (e) => e.message, 'message', contains('disposed'))), ); diff --git a/test/use_reassemble_test.dart b/test/use_reassemble_test.dart index dab6e519..3cc7adce 100644 --- a/test/use_reassemble_test.dart +++ b/test/use_reassemble_test.dart @@ -5,17 +5,6 @@ import 'package:flutter_hooks/flutter_hooks.dart'; import 'mock.dart'; void main() { - testWidgets('useReassemble null callback throws', (tester) async { - await tester.pumpWidget( - HookBuilder(builder: (c) { - useReassemble(null); - return Container(); - }), - ); - - expect(tester.takeException(), isAssertionError); - }); - testWidgets("hot-reload calls useReassemble's callback", (tester) async { final reassemble = MockReassemble(); diff --git a/test/use_reducer_test.dart b/test/use_reducer_test.dart index 5b31a2ec..753d6426 100644 --- a/test/use_reducer_test.dart +++ b/test/use_reducer_test.dart @@ -8,7 +8,11 @@ void main() { testWidgets('debugFillProperties', (tester) async { await tester.pumpWidget( HookBuilder(builder: (context) { - useReducer((state, action) => 42); + useReducer( + (state, action) => 42, + initialAction: null, + initialState: null, + ); return const SizedBox(); }), ); @@ -28,35 +32,109 @@ void main() { }); group('useReducer', () { - testWidgets('basic', (tester) async { + testWidgets('supports null initial state', (tester) async { + Store? store; + + await tester.pumpWidget( + HookBuilder( + builder: (context) { + store = useReducer( + (state, action) => state, + initialAction: null, + initialState: null, + ); + + return Container(); + }, + ), + ); + + expect(store!.state, isNull); + }); + + testWidgets('supports null state after dispatch', (tester) async { + Store? store; + + await tester.pumpWidget( + HookBuilder( + builder: (context) { + store = useReducer( + (state, action) => action, + initialAction: 0, + initialState: null, + ); + + return Container(); + }, + ), + ); + + expect(store?.state, 0); + + store!.dispatch(null); + + expect(store!.state, null); + }); + + testWidgets('initialize the state even "state" is never read', + (tester) async { final reducer = MockReducer(); - Store store; - Future pump() { - return tester.pumpWidget(HookBuilder( + await tester.pumpWidget( + HookBuilder( builder: (context) { - store = useReducer(reducer); + useReducer( + reducer, + initialAction: '', + initialState: 0, + ); return Container(); }, - )); + ), + ); + + verify(reducer(null, null)).called(1); + verifyNoMoreInteractions(reducer); + }); + + testWidgets('basic', (tester) async { + final reducer = MockReducer(); + + Store? store; + + Future pump() { + return tester.pumpWidget( + HookBuilder( + builder: (context) { + store = useReducer( + reducer, + initialAction: null, + initialState: null, + ); + return Container(); + }, + ), + ); } when(reducer(null, null)).thenReturn(0); + await pump(); final element = tester.firstElement(find.byType(HookBuilder)); verify(reducer(null, null)).called(1); verifyNoMoreInteractions(reducer); - expect(store.state, 0); + expect(store!.state, 0); await pump(); + verifyNoMoreInteractions(reducer); - expect(store.state, 0); + expect(store!.state, 0); when(reducer(0, 'foo')).thenReturn(1); - store.dispatch('foo'); + store!.dispatch('foo'); verify(reducer(0, 'foo')).called(1); verifyNoMoreInteractions(reducer); @@ -66,40 +144,32 @@ void main() { when(reducer(1, 'bar')).thenReturn(1); - store.dispatch('bar'); + store!.dispatch('bar'); verify(reducer(1, 'bar')).called(1); verifyNoMoreInteractions(reducer); expect(element.dirty, false); }); - testWidgets('reducer required', (tester) async { - await tester.pumpWidget( - HookBuilder( - builder: (context) { - useReducer(null); - return Container(); - }, - ), - ); - - expect(tester.takeException(), isAssertionError); - }); - - testWidgets('dispatch during build fails', (tester) async { - final reducer = MockReducer(); + testWidgets('dispatch during build works', (tester) async { + Store? store; await tester.pumpWidget( HookBuilder( builder: (context) { - useReducer(reducer.call).dispatch('Foo'); + store = useReducer( + (state, action) => action, + initialAction: 0, + initialState: null, + )..dispatch(42); return Container(); }, ), ); - expect(tester.takeException(), isAssertionError); + expect(store!.state, 42); }); + testWidgets('first reducer call receive initialAction and initialState', (tester) async { final reducer = MockReducer(); @@ -108,7 +178,7 @@ void main() { await tester.pumpWidget( HookBuilder( builder: (context) { - final result = useReducer( + final result = useReducer( reducer, initialAction: 'Foo', initialState: 0, @@ -120,47 +190,11 @@ void main() { expect(find.text('42'), findsOneWidget); }); - testWidgets('dispatchs reducer call must not return null', (tester) async { - final reducer = MockReducer(); - - Store store; - Future pump() { - return tester.pumpWidget(HookBuilder( - builder: (context) { - store = useReducer(reducer); - return Container(); - }, - )); - } - - when(reducer(null, null)).thenReturn(42); - - await pump(); - - when(reducer(42, 'foo')).thenReturn(null); - expect(() => store.dispatch('foo'), throwsAssertionError); - - await pump(); - expect(store.state, 42); - }); - - testWidgets('first reducer call must not return null', (tester) async { - final reducer = MockReducer(); - - await tester.pumpWidget( - HookBuilder( - builder: (context) { - useReducer(reducer.call); - return Container(); - }, - ), - ); - - expect(tester.takeException(), isAssertionError); - }); }); } class MockReducer extends Mock { - int call(int state, String action); + int? call(int? state, String? action) { + return super.noSuchMethod(Invocation.getter(#call), 0) as int?; + } } diff --git a/test/use_scroll_controller_test.dart b/test/use_scroll_controller_test.dart index b926ab1c..5657d2b5 100644 --- a/test/use_scroll_controller_test.dart +++ b/test/use_scroll_controller_test.dart @@ -32,8 +32,8 @@ void main() { group('useScrollController', () { testWidgets('initial values matches with real constructor', (tester) async { - ScrollController controller; - ScrollController controller2; + late ScrollController controller; + late ScrollController controller2; await tester.pumpWidget( HookBuilder(builder: (context) { @@ -49,8 +49,8 @@ void main() { }); testWidgets("returns a ScrollController that doesn't change", (tester) async { - ScrollController controller; - ScrollController controller2; + late ScrollController controller; + late ScrollController controller2; await tester.pumpWidget( HookBuilder(builder: (context) { @@ -73,7 +73,7 @@ void main() { testWidgets('passes hook parameters to the ScrollController', (tester) async { - ScrollController controller; + late ScrollController controller; await tester.pumpWidget( HookBuilder( diff --git a/test/use_state_test.dart b/test/use_state_test.dart index c42240f3..4c32073f 100644 --- a/test/use_state_test.dart +++ b/test/use_state_test.dart @@ -7,8 +7,8 @@ import 'mock.dart'; void main() { testWidgets('useState basic', (tester) async { - ValueNotifier state; - HookElement element; + late ValueNotifier state; + late HookElement element; await tester.pumpWidget(HookBuilder( builder: (context) { @@ -41,13 +41,13 @@ void main() { }); testWidgets('no initial data', (tester) async { - ValueNotifier state; - HookElement element; + late ValueNotifier state; + late HookElement element; await tester.pumpWidget(HookBuilder( builder: (context) { element = context as HookElement; - state = useState(); + state = useState(null); return Container(); }, )); @@ -75,8 +75,8 @@ void main() { }); testWidgets('debugFillProperties should print state hook ', (tester) async { - ValueNotifier state; - HookElement element; + late ValueNotifier state; + late HookElement element; final hookWidget = HookBuilder( builder: (context) { element = context as HookElement; diff --git a/test/use_stream_controller_test.dart b/test/use_stream_controller_test.dart index 2950342d..0841d123 100644 --- a/test/use_stream_controller_test.dart +++ b/test/use_stream_controller_test.dart @@ -35,7 +35,7 @@ void main() { group('useStreamController', () { testWidgets('keys', (tester) async { - StreamController controller; + late StreamController controller; await tester.pumpWidget(HookBuilder(builder: (context) { controller = useStreamController(); @@ -51,7 +51,7 @@ void main() { expect(previous, isNot(controller)); }); testWidgets('basics', (tester) async { - StreamController controller; + late StreamController controller; await tester.pumpWidget(HookBuilder(builder: (context) { controller = useStreamController(); @@ -88,7 +88,7 @@ void main() { expect(controller.isClosed, true); }); testWidgets('sync', (tester) async { - StreamController controller; + late StreamController controller; await tester.pumpWidget(HookBuilder(builder: (context) { controller = useStreamController(sync: true); diff --git a/test/use_stream_test.dart b/test/use_stream_test.dart index 46a83ff0..8dd1ab2e 100644 --- a/test/use_stream_test.dart +++ b/test/use_stream_test.dart @@ -16,7 +16,7 @@ void main() { await tester.pumpWidget( HookBuilder(builder: (context) { - useStream(stream); + useStream(stream, initialData: 42); return const SizedBox(); }), ); @@ -31,7 +31,8 @@ void main() { .toStringDeep(), equalsIgnoringHashCodes( 'HookBuilder\n' - ' │ useStream: AsyncSnapshot(ConnectionState.done, 42, null)\n' + ' │ useStream: AsyncSnapshot(ConnectionState.done, 42, null,\n' + ' │ null)\n' ' └SizedBox(renderObject: RenderConstrainedBox#00000)\n', ), ); @@ -39,93 +40,74 @@ void main() { testWidgets('default preserve state, changing stream keeps previous value', (tester) async { - AsyncSnapshot value; - Widget Function(BuildContext) builder(Stream stream) { + late AsyncSnapshot? value; + Widget Function(BuildContext) builder(Stream stream) { return (context) { - value = useStream(stream); + value = useStream(stream, initialData: null); return Container(); }; } var stream = Stream.fromFuture(Future.value(0)); await tester.pumpWidget(HookBuilder(builder: builder(stream))); - expect(value.data, null); + expect(value!.data, null); await tester.pumpWidget(HookBuilder(builder: builder(stream))); - expect(value.data, 0); + expect(value!.data, 0); stream = Stream.fromFuture(Future.value(42)); await tester.pumpWidget(HookBuilder(builder: builder(stream))); - expect(value.data, 0); + expect(value!.data, 0); await tester.pumpWidget(HookBuilder(builder: builder(stream))); - expect(value.data, 42); + expect(value!.data, 42); }); testWidgets('If preserveState == false, changing stream resets value', (tester) async { - AsyncSnapshot value; - Widget Function(BuildContext) builder(Stream stream) { + late AsyncSnapshot? value; + Widget Function(BuildContext) builder(Stream stream) { return (context) { - value = useStream(stream, preserveState: false); + value = useStream(stream, initialData: null, preserveState: false); return Container(); }; } var stream = Stream.fromFuture(Future.value(0)); await tester.pumpWidget(HookBuilder(builder: builder(stream))); - expect(value.data, null); + expect(value!.data, null); await tester.pumpWidget(HookBuilder(builder: builder(stream))); - expect(value.data, 0); + expect(value!.data, 0); stream = Stream.fromFuture(Future.value(42)); await tester.pumpWidget(HookBuilder(builder: builder(stream))); - expect(value.data, null); + expect(value!.data, null); await tester.pumpWidget(HookBuilder(builder: builder(stream))); - expect(value.data, 42); + expect(value!.data, 42); }); Widget Function(BuildContext) snapshotText(Stream stream, - {String initialData}) { + {String? initialData}) { return (context) { - final snapshot = useStream(stream, initialData: initialData); + final snapshot = useStream(stream, initialData: initialData ?? ''); return Text(snapshot.toString(), textDirection: TextDirection.ltr); }; } - testWidgets('gracefully handles transition from null stream', (tester) async { - await tester.pumpWidget(HookBuilder(builder: snapshotText(null))); - expect(find.text('AsyncSnapshot(ConnectionState.none, null, null)'), - findsOneWidget); - final controller = StreamController(); - await tester - .pumpWidget(HookBuilder(builder: snapshotText(controller.stream))); - expect( - find.text('AsyncSnapshot(ConnectionState.waiting, null, null)'), - findsOneWidget); - }); - testWidgets('gracefully handles transition to null stream', (tester) async { - final controller = StreamController(); - await tester - .pumpWidget(HookBuilder(builder: snapshotText(controller.stream))); - expect( - find.text('AsyncSnapshot(ConnectionState.waiting, null, null)'), - findsOneWidget); - await tester.pumpWidget(HookBuilder(builder: snapshotText(null))); - expect(find.text('AsyncSnapshot(ConnectionState.none, null, null)'), - findsOneWidget); - }); testWidgets('gracefully handles transition to other stream', (tester) async { final controllerA = StreamController(); final controllerB = StreamController(); await tester .pumpWidget(HookBuilder(builder: snapshotText(controllerA.stream))); expect( - find.text('AsyncSnapshot(ConnectionState.waiting, null, null)'), + find.text( + 'AsyncSnapshot(ConnectionState.waiting, , null, null)'), findsOneWidget); await tester .pumpWidget(HookBuilder(builder: snapshotText(controllerB.stream))); controllerB.add('B'); controllerA.add('A'); await eventFiring(tester); - expect(find.text('AsyncSnapshot(ConnectionState.active, B, null)'), + expect( + find.text( + 'AsyncSnapshot(ConnectionState.active, B, null, null)'), findsOneWidget); }); testWidgets('tracks events and errors of stream until completion', @@ -134,23 +116,27 @@ void main() { await tester .pumpWidget(HookBuilder(builder: snapshotText(controller.stream))); expect( - find.text('AsyncSnapshot(ConnectionState.waiting, null, null)'), + find.text( + 'AsyncSnapshot(ConnectionState.waiting, , null, null)'), findsOneWidget); controller..add('1')..add('2'); await eventFiring(tester); - expect(find.text('AsyncSnapshot(ConnectionState.active, 2, null)'), + expect( + find.text( + 'AsyncSnapshot(ConnectionState.active, 2, null, null)'), findsOneWidget); controller ..add('3') ..addError('bad'); await eventFiring(tester); expect( - find.text('AsyncSnapshot(ConnectionState.active, null, bad)'), + find.text('AsyncSnapshot(ConnectionState.active, null, bad, )'), findsOneWidget); controller.add('4'); await controller.close(); await eventFiring(tester); - expect(find.text('AsyncSnapshot(ConnectionState.done, 4, null)'), + expect( + find.text('AsyncSnapshot(ConnectionState.done, 4, null, null)'), findsOneWidget); }); testWidgets('runs the builder using given initial data', (tester) async { @@ -158,20 +144,26 @@ void main() { await tester.pumpWidget(HookBuilder( builder: snapshotText(controller.stream, initialData: 'I'), )); - expect(find.text('AsyncSnapshot(ConnectionState.waiting, I, null)'), + expect( + find.text( + 'AsyncSnapshot(ConnectionState.waiting, I, null, null)'), findsOneWidget); }); testWidgets('ignores initialData when reconfiguring', (tester) async { await tester.pumpWidget(HookBuilder( - builder: snapshotText(null, initialData: 'I'), + builder: snapshotText(const Stream.empty(), initialData: 'I'), )); - expect(find.text('AsyncSnapshot(ConnectionState.none, I, null)'), + expect( + find.text( + 'AsyncSnapshot(ConnectionState.waiting, I, null, null)'), findsOneWidget); final controller = StreamController(); await tester.pumpWidget(HookBuilder( builder: snapshotText(controller.stream, initialData: 'Ignored'), )); - expect(find.text('AsyncSnapshot(ConnectionState.waiting, I, null)'), + expect( + find.text( + 'AsyncSnapshot(ConnectionState.waiting, I, null, null)'), findsOneWidget); }); } diff --git a/test/use_tab_controller_test.dart b/test/use_tab_controller_test.dart index 1a66215e..2adf1784 100644 --- a/test/use_tab_controller_test.dart +++ b/test/use_tab_controller_test.dart @@ -35,8 +35,8 @@ void main() { group('useTabController', () { testWidgets('initial values matches with real constructor', (tester) async { - TabController controller; - TabController controller2; + late TabController controller; + late TabController controller2; await tester.pumpWidget( HookBuilder(builder: (context) { @@ -50,8 +50,8 @@ void main() { expect(controller.index, controller2.index); }); testWidgets("returns a TabController that doesn't change", (tester) async { - TabController controller; - TabController controller2; + late TabController controller; + late TabController controller2; await tester.pumpWidget( HookBuilder(builder: (context) { @@ -72,7 +72,7 @@ void main() { expect(identical(controller, controller2), isTrue); }); testWidgets('changing length is no-op', (tester) async { - TabController controller; + late TabController controller; await tester.pumpWidget( HookBuilder(builder: (context) { @@ -94,7 +94,7 @@ void main() { }); testWidgets('passes hook parameters to the TabController', (tester) async { - TabController controller; + late TabController controller; await tester.pumpWidget( HookBuilder( @@ -112,7 +112,7 @@ void main() { testWidgets('allows passing custom vsync', (tester) async { final vsync = TickerProviderMock(); final ticker = Ticker((_) {}); - when(vsync.createTicker(any)).thenReturn(ticker); + when(vsync.createTicker((_) {})).thenReturn(ticker); await tester.pumpWidget( HookBuilder( @@ -124,7 +124,7 @@ void main() { ), ); - verify(vsync.createTicker(any)).called(1); + verify(vsync.createTicker((_) {})).called(1); verifyNoMoreInteractions(vsync); await tester.pumpWidget( @@ -142,4 +142,9 @@ void main() { }); } -class TickerProviderMock extends Mock implements TickerProvider {} +class TickerProviderMock extends Mock implements TickerProvider { + @override + Ticker createTicker(TickerCallback onTick) => + super.noSuchMethod(Invocation.getter(#createTicker), Ticker(onTick)) + as Ticker; +} diff --git a/test/use_text_editing_controller_test.dart b/test/use_text_editing_controller_test.dart index 2a5612fb..ce119ca8 100644 --- a/test/use_text_editing_controller_test.dart +++ b/test/use_text_editing_controller_test.dart @@ -37,7 +37,7 @@ void main() { testWidgets('useTextEditingController returns a controller', (tester) async { final rebuilder = ValueNotifier(0); - TextEditingController controller; + late TextEditingController controller; await tester.pumpWidget(HookBuilder( builder: (context) { @@ -61,7 +61,7 @@ void main() { await tester.pumpWidget(Container()); expect( - () => controller.addListener(null), + () => controller.addListener(() {}), throwsA(isFlutterError.having( (e) => e.message, 'message', contains('disposed'))), ); @@ -69,7 +69,7 @@ void main() { testWidgets('respects initial text property', (tester) async { final rebuilder = ValueNotifier(0); - TextEditingController controller; + late TextEditingController controller; const initialText = 'hello hooks'; var targetText = initialText; @@ -83,27 +83,13 @@ void main() { expect(controller.text, targetText); - // change text and rebuild - the value of the controler shouldn't change + // change text and rebuild - the value of the controller shouldn't change targetText = "can't see me!"; rebuilder.notifyListeners(); await tester.pumpAndSettle(); expect(controller.text, initialText); }); - testWidgets('useTextEditingController throws error on null value', - (tester) async { - await tester.pumpWidget(HookBuilder( - builder: (context) { - try { - useTextEditingController.fromValue(null); - } catch (e) { - expect(e, isAssertionError); - } - return Container(); - }, - )); - }); - testWidgets('respects initial value property', (tester) async { final rebuilder = ValueNotifier(0); const initialValue = TextEditingValue( @@ -111,7 +97,7 @@ void main() { selection: TextSelection.collapsed(offset: 2), ); var targetValue = initialValue; - TextEditingController controller; + late TextEditingController controller; await tester.pumpWidget(HookBuilder( builder: (context) { diff --git a/test/use_ticker_provider_test.dart b/test/use_ticker_provider_test.dart index 8105519a..8efdb612 100644 --- a/test/use_ticker_provider_test.dart +++ b/test/use_ticker_provider_test.dart @@ -31,7 +31,7 @@ void main() { }); testWidgets('useSingleTickerProvider basic', (tester) async { - TickerProvider provider; + late TickerProvider provider; await tester.pumpWidget(TickerMode( enabled: true, @@ -62,7 +62,7 @@ void main() { }); testWidgets('useSingleTickerProvider still active', (tester) async { - TickerProvider provider; + late TickerProvider provider; await tester.pumpWidget(TickerMode( enabled: true, @@ -90,8 +90,8 @@ void main() { }); testWidgets('useSingleTickerProvider pass down keys', (tester) async { - TickerProvider provider; - List keys; + late TickerProvider provider; + List? keys; await tester.pumpWidget(HookBuilder(builder: (context) { provider = useSingleTickerProvider(keys: keys); diff --git a/test/use_value_changed_test.dart b/test/use_value_changed_test.dart index a755df45..8ffca397 100644 --- a/test/use_value_changed_test.dart +++ b/test/use_value_changed_test.dart @@ -38,7 +38,7 @@ void main() { testWidgets('useValueChanged basic', (tester) async { var value = 42; final _useValueChanged = MockValueChanged(); - String result; + late String? result; Future pump() { return tester.pumpWidget( @@ -96,19 +96,8 @@ void main() { // dispose await tester.pumpWidget(const SizedBox()); }); - - testWidgets('valueChanged required', (tester) async { - await tester.pumpWidget(HookBuilder( - builder: (context) { - useValueChanged(42, null); - return Container(); - }, - )); - - expect(tester.takeException(), isAssertionError); - }); } class MockValueChanged extends Mock { - String call(int value, String previous); + String? call(int? value, String? previous); } diff --git a/test/use_value_listenable_test.dart b/test/use_value_listenable_test.dart index 821bcc1a..b388f901 100644 --- a/test/use_value_listenable_test.dart +++ b/test/use_value_listenable_test.dart @@ -26,22 +26,9 @@ void main() { ), ); }); - - testWidgets('useValueListenable throws with null', (tester) async { - await tester.pumpWidget( - HookBuilder( - builder: (context) { - useValueListenable(null); - return Container(); - }, - ), - ); - - expect(tester.takeException(), isAssertionError); - }); testWidgets('useValueListenable', (tester) async { var listenable = ValueNotifier(0); - int result; + late int result; Future pump() { return tester.pumpWidget(HookBuilder( diff --git a/test/use_value_notifier_test.dart b/test/use_value_notifier_test.dart index bad9ee8f..26c0f4d7 100644 --- a/test/use_value_notifier_test.dart +++ b/test/use_value_notifier_test.dart @@ -29,8 +29,8 @@ void main() { group('useValueNotifier', () { testWidgets('useValueNotifier basic', (tester) async { - ValueNotifier state; - HookElement element; + late ValueNotifier state; + late HookElement element; final listener = MockListener(); await tester.pumpWidget(HookBuilder( @@ -71,14 +71,14 @@ void main() { }); testWidgets('no initial data', (tester) async { - ValueNotifier state; - HookElement element; + late ValueNotifier state; + late HookElement element; final listener = MockListener(); await tester.pumpWidget(HookBuilder( builder: (context) { element = context as HookElement; - state = useValueNotifier(); + state = useValueNotifier(null); return Container(); }, )); @@ -113,8 +113,8 @@ void main() { }); testWidgets('creates new valuenotifier when key change', (tester) async { - ValueNotifier state; - ValueNotifier previous; + late ValueNotifier state; + late ValueNotifier previous; await tester.pumpWidget(HookBuilder( builder: (context) { @@ -133,13 +133,14 @@ void main() { expect(state, isNot(previous)); }); - testWidgets("instance stays the same when key don' change", (tester) async { - ValueNotifier state; - ValueNotifier previous; + testWidgets("instance stays the same when keys don't change", + (tester) async { + late ValueNotifier state; + late ValueNotifier previous; await tester.pumpWidget(HookBuilder( builder: (context) { - state = useValueNotifier(null, [42]); + state = useValueNotifier(0, [42]); return Container(); }, )); From bede65523cb6283b6750aa1e137e6f3b57046361 Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Wed, 27 Jan 2021 11:53:08 +0000 Subject: [PATCH 214/384] update all_lint_rules --- all_lint_rules.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/all_lint_rules.yaml b/all_lint_rules.yaml index 5b49ce51..93d14856 100644 --- a/all_lint_rules.yaml +++ b/all_lint_rules.yaml @@ -14,6 +14,7 @@ linter: - avoid_catching_errors - avoid_classes_with_only_static_members - avoid_double_and_int_checks + - avoid_dynamic_calls - avoid_empty_else - avoid_equals_and_hash_code_on_mutable_classes - avoid_escaping_inner_quotes From 4c4881a0a863eb536476e2e050b1d7046ff088fa Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Wed, 17 Feb 2021 21:30:31 +0000 Subject: [PATCH 215/384] Fix CI --- example/lib/use_effect.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/example/lib/use_effect.dart b/example/lib/use_effect.dart index 1cfebc3c..f0fcf1e9 100644 --- a/example/lib/use_effect.dart +++ b/example/lib/use_effect.dart @@ -78,7 +78,7 @@ StreamController _useLocalStorageInt( // to the controller useEffect( () { - SharedPreferences.getInstance().then((prefs) async { + SharedPreferences.getInstance().then((prefs) async { final int valueFromStorage = prefs.getInt(key); controller.add(valueFromStorage ?? defaultValue); }).catchError(controller.addError); From abb7d89f05184fa28243a0b7b8bb8dc64f707169 Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Wed, 17 Feb 2021 21:38:05 +0000 Subject: [PATCH 216/384] upgrade mockito --- pubspec.yaml | 2 +- test/memoized_test.dart | 5 ++++- test/mock.dart | 6 ++++-- test/pre_build_abort_test.dart | 5 ++++- test/use_animation_controller_test.dart | 7 ++++--- test/use_reducer_test.dart | 5 ++++- test/use_tab_controller_test.dart | 7 ++++--- 7 files changed, 25 insertions(+), 12 deletions(-) diff --git a/pubspec.yaml b/pubspec.yaml index adaaaf23..965b10ad 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -14,4 +14,4 @@ dependencies: dev_dependencies: flutter_test: sdk: flutter - mockito: "^5.0.0-nullsafety.4" + mockito: "^5.0.0-nullsafety.5" diff --git a/test/memoized_test.dart b/test/memoized_test.dart index 0e06a35f..5ff48c85 100644 --- a/test/memoized_test.dart +++ b/test/memoized_test.dart @@ -289,5 +289,8 @@ void main() { } class MockValueBuilder extends Mock { - int call() => super.noSuchMethod(Invocation.getter(#call), 42) as int; + int call() => super.noSuchMethod( + Invocation.getter(#call), + returnValue: 42, + ) as int; } diff --git a/test/mock.dart b/test/mock.dart index 8fa8dd7c..21f8bb98 100644 --- a/test/mock.dart +++ b/test/mock.dart @@ -129,8 +129,10 @@ class MockFlutterErrorDetails extends Mock implements FlutterErrorDetails { } class MockErrorBuilder extends Mock { - Widget call(FlutterErrorDetails error) => - super.noSuchMethod(Invocation.getter(#call), Container()) as Widget; + Widget call(FlutterErrorDetails error) => super.noSuchMethod( + Invocation.getter(#call), + returnValue: Container(), + ) as Widget; } class MockOnError extends Mock { diff --git a/test/pre_build_abort_test.dart b/test/pre_build_abort_test.dart index 68713ede..b6726365 100644 --- a/test/pre_build_abort_test.dart +++ b/test/pre_build_abort_test.dart @@ -339,5 +339,8 @@ class MayRebuildState extends HookState { } class ShouldRebuildMock extends Mock { - bool call() => super.noSuchMethod(Invocation.getter(#call), false) as bool; + bool call() => super.noSuchMethod( + Invocation.getter(#call), + returnValue: false, + ) as bool; } diff --git a/test/use_animation_controller_test.dart b/test/use_animation_controller_test.dart index 6aea37bd..d424a941 100644 --- a/test/use_animation_controller_test.dart +++ b/test/use_animation_controller_test.dart @@ -194,9 +194,10 @@ void main() { class _TickerProvider extends Mock implements TickerProvider { @override - Ticker createTicker(TickerCallback onTick) => - super.noSuchMethod(Invocation.getter(#createTicker), Ticker(onTick)) - as Ticker; + Ticker createTicker(TickerCallback onTick) => super.noSuchMethod( + Invocation.getter(#createTicker), + returnValue: Ticker(onTick), + ) as Ticker; } class MockEffect extends Mock { diff --git a/test/use_reducer_test.dart b/test/use_reducer_test.dart index 753d6426..b620e4b8 100644 --- a/test/use_reducer_test.dart +++ b/test/use_reducer_test.dart @@ -195,6 +195,9 @@ void main() { class MockReducer extends Mock { int? call(int? state, String? action) { - return super.noSuchMethod(Invocation.getter(#call), 0) as int?; + return super.noSuchMethod( + Invocation.getter(#call), + returnValue: 0, + ) as int?; } } diff --git a/test/use_tab_controller_test.dart b/test/use_tab_controller_test.dart index 2adf1784..83bc978f 100644 --- a/test/use_tab_controller_test.dart +++ b/test/use_tab_controller_test.dart @@ -144,7 +144,8 @@ void main() { class TickerProviderMock extends Mock implements TickerProvider { @override - Ticker createTicker(TickerCallback onTick) => - super.noSuchMethod(Invocation.getter(#createTicker), Ticker(onTick)) - as Ticker; + Ticker createTicker(TickerCallback onTick) => super.noSuchMethod( + Invocation.getter(#createTicker), + returnValue: Ticker(onTick), + ) as Ticker; } From 178b2f5d1eccee4700967c3e94341458d18e3e79 Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Wed, 17 Feb 2021 21:42:23 +0000 Subject: [PATCH 217/384] v0.16.0 --- CHANGELOG.md | 4 ++++ pubspec.yaml | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3c5c249f..66b06179 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.16.0 + +Stable null-safety release + ## 0.16.0-nullsafety.0 Migrated flutter_hooks to null-safety (special thanks to @DevNico for the help!) diff --git a/pubspec.yaml b/pubspec.yaml index 965b10ad..6b262c8d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: flutter_hooks description: A flutter implementation of React hooks. It adds a new kind of widget with enhanced code reuse. homepage: https://github.com/rrousselGit/flutter_hooks -version: 0.16.0-nullsafety.0 +version: 0.16.0 environment: sdk: ">=2.12.0-0 <3.0.0" From e260a27425f436c48e648aba4d9ab3a783bb60b1 Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Wed, 17 Feb 2021 21:43:12 +0000 Subject: [PATCH 218/384] remove upper flutter constraint --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index 6b262c8d..d451ac17 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -5,7 +5,7 @@ version: 0.16.0 environment: sdk: ">=2.12.0-0 <3.0.0" - flutter: ">=1.20.0 <2.0.0" + flutter: ">=1.20.0" dependencies: flutter: From b7a756af2429f8f862c410ab7c05b1cf775b80ab Mon Sep 17 00:00:00 2001 From: Subhendu Kundu Date: Sun, 7 Mar 2021 15:08:44 +0530 Subject: [PATCH 219/384] fix: changed to swapi.dev api (#221) https://swapi.co seems not accessiable anymore. Switching to https://swapi.dev --- example/lib/star_wars/star_wars_api.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/example/lib/star_wars/star_wars_api.dart b/example/lib/star_wars/star_wars_api.dart index a53c60b7..ed48693d 100644 --- a/example/lib/star_wars/star_wars_api.dart +++ b/example/lib/star_wars/star_wars_api.dart @@ -8,7 +8,7 @@ import 'models.dart'; class StarWarsApi { /// load and return one page of planets Future getPlanets([String page]) async { - page ??= 'https://swapi.co/api/planets'; + page ??= 'https://swapi.dev/api/planets'; final response = await http.get(page); final dynamic json = jsonDecode(utf8.decode(response.bodyBytes)); From 03370e63aa16ecaf76a9caa1a9dd1a4dd2174fd1 Mon Sep 17 00:00:00 2001 From: Jimmy Aumard Date: Sun, 21 Mar 2021 19:02:59 +0100 Subject: [PATCH 220/384] move to multi package structure (#228) --- .github/workflows/build.yml | 11 +- README.md | 4 +- .../flutter_hooks/.gitignore | 0 .../flutter_hooks/CHANGELOG.md | 0 packages/flutter_hooks/LICENSE | 1 + packages/flutter_hooks/README.md | 1 + .../flutter_hooks/all_lint_rules.yaml | 0 .../flutter_hooks/analysis_options.yaml | 0 .../flutter_hooks/example}/.gitignore | 0 .../flutter_hooks/example}/.metadata | 0 .../flutter_hooks/example}/README.md | 0 .../example}/analysis_options.yaml | 0 .../example}/lib/custom_hook_function.dart | 0 .../flutter_hooks/example}/lib/main.dart | 0 .../example}/lib/star_wars/README.md | 0 .../example}/lib/star_wars/models.dart | 0 .../example}/lib/star_wars/models.g.dart | 0 .../example}/lib/star_wars/planet_screen.dart | 0 .../example}/lib/star_wars/redux.dart | 0 .../example}/lib/star_wars/redux.g.dart | 0 .../example}/lib/star_wars/star_wars_api.dart | 0 .../example}/lib/use_effect.dart | 0 .../example}/lib/use_reducer.dart | 116 +++++++++--------- .../flutter_hooks/example}/lib/use_state.dart | 0 .../example}/lib/use_stream.dart | 0 .../flutter_hooks/example}/pubspec.yaml | 0 .../flutter_hooks/flutter-hook.svg | 0 .../flutter_hooks/lib}/flutter_hooks.dart | 0 .../flutter_hooks/lib}/src/animation.dart | 0 .../flutter_hooks/lib}/src/async.dart | 0 .../flutter_hooks/lib}/src/focus.dart | 0 .../flutter_hooks/lib}/src/framework.dart | 0 .../flutter_hooks/lib}/src/hooks.dart | 0 .../flutter_hooks/lib}/src/listenable.dart | 0 .../flutter_hooks/lib}/src/misc.dart | 0 .../lib}/src/page_controller.dart | 0 .../flutter_hooks/lib}/src/primitives.dart | 0 .../lib}/src/scroll_controller.dart | 0 .../lib}/src/tab_controller.dart | 0 .../lib}/src/text_controller.dart | 0 .../flutter_hooks/pubspec.yaml | 0 .../resources}/translations/pt_br/README.md | 4 +- .../test}/hook_builder_test.dart | 0 .../flutter_hooks/test}/hook_widget_test.dart | 0 .../flutter_hooks/test}/is_mounted_test.dart | 0 .../flutter_hooks/test}/memoized_test.dart | 0 .../flutter_hooks/test}/mock.dart | 0 .../test}/pre_build_abort_test.dart | 0 .../test}/use_animation_controller_test.dart | 0 .../test}/use_animation_test.dart | 0 .../flutter_hooks/test}/use_context_test.dart | 0 .../flutter_hooks/test}/use_effect_test.dart | 0 .../test}/use_focus_node_test.dart | 0 .../flutter_hooks/test}/use_future_test.dart | 0 .../test}/use_listenable_test.dart | 0 .../test}/use_page_controller_test.dart | 0 .../test}/use_previous_test.dart | 0 .../test}/use_reassemble_test.dart | 0 .../flutter_hooks/test}/use_reducer_test.dart | 0 .../test}/use_scroll_controller_test.dart | 0 .../flutter_hooks/test}/use_state_test.dart | 0 .../test}/use_stream_controller_test.dart | 0 .../flutter_hooks/test}/use_stream_test.dart | 0 .../test}/use_tab_controller_test.dart | 0 .../use_text_editing_controller_test.dart | 0 .../test}/use_ticker_provider_test.dart | 0 .../test}/use_value_changed_test.dart | 0 .../test}/use_value_listenable_test.dart | 0 .../test}/use_value_notifier_test.dart | 0 69 files changed, 71 insertions(+), 66 deletions(-) rename .gitignore => packages/flutter_hooks/.gitignore (100%) rename CHANGELOG.md => packages/flutter_hooks/CHANGELOG.md (100%) create mode 120000 packages/flutter_hooks/LICENSE create mode 120000 packages/flutter_hooks/README.md rename all_lint_rules.yaml => packages/flutter_hooks/all_lint_rules.yaml (100%) rename analysis_options.yaml => packages/flutter_hooks/analysis_options.yaml (100%) rename {example => packages/flutter_hooks/example}/.gitignore (100%) rename {example => packages/flutter_hooks/example}/.metadata (100%) rename {example => packages/flutter_hooks/example}/README.md (100%) rename {example => packages/flutter_hooks/example}/analysis_options.yaml (100%) rename {example => packages/flutter_hooks/example}/lib/custom_hook_function.dart (100%) rename {example => packages/flutter_hooks/example}/lib/main.dart (100%) rename {example => packages/flutter_hooks/example}/lib/star_wars/README.md (100%) rename {example => packages/flutter_hooks/example}/lib/star_wars/models.dart (100%) rename {example => packages/flutter_hooks/example}/lib/star_wars/models.g.dart (100%) rename {example => packages/flutter_hooks/example}/lib/star_wars/planet_screen.dart (100%) rename {example => packages/flutter_hooks/example}/lib/star_wars/redux.dart (100%) rename {example => packages/flutter_hooks/example}/lib/star_wars/redux.g.dart (100%) rename {example => packages/flutter_hooks/example}/lib/star_wars/star_wars_api.dart (100%) rename {example => packages/flutter_hooks/example}/lib/use_effect.dart (100%) rename {example => packages/flutter_hooks/example}/lib/use_reducer.dart (97%) rename {example => packages/flutter_hooks/example}/lib/use_state.dart (100%) rename {example => packages/flutter_hooks/example}/lib/use_stream.dart (100%) rename {example => packages/flutter_hooks/example}/pubspec.yaml (100%) rename flutter-hook.svg => packages/flutter_hooks/flutter-hook.svg (100%) rename {lib => packages/flutter_hooks/lib}/flutter_hooks.dart (100%) rename {lib => packages/flutter_hooks/lib}/src/animation.dart (100%) rename {lib => packages/flutter_hooks/lib}/src/async.dart (100%) rename {lib => packages/flutter_hooks/lib}/src/focus.dart (100%) rename {lib => packages/flutter_hooks/lib}/src/framework.dart (100%) rename {lib => packages/flutter_hooks/lib}/src/hooks.dart (100%) rename {lib => packages/flutter_hooks/lib}/src/listenable.dart (100%) rename {lib => packages/flutter_hooks/lib}/src/misc.dart (100%) rename {lib => packages/flutter_hooks/lib}/src/page_controller.dart (100%) rename {lib => packages/flutter_hooks/lib}/src/primitives.dart (100%) rename {lib => packages/flutter_hooks/lib}/src/scroll_controller.dart (100%) rename {lib => packages/flutter_hooks/lib}/src/tab_controller.dart (100%) rename {lib => packages/flutter_hooks/lib}/src/text_controller.dart (100%) rename pubspec.yaml => packages/flutter_hooks/pubspec.yaml (100%) rename {resources => packages/flutter_hooks/resources}/translations/pt_br/README.md (98%) rename {test => packages/flutter_hooks/test}/hook_builder_test.dart (100%) rename {test => packages/flutter_hooks/test}/hook_widget_test.dart (100%) rename {test => packages/flutter_hooks/test}/is_mounted_test.dart (100%) rename {test => packages/flutter_hooks/test}/memoized_test.dart (100%) rename {test => packages/flutter_hooks/test}/mock.dart (100%) rename {test => packages/flutter_hooks/test}/pre_build_abort_test.dart (100%) rename {test => packages/flutter_hooks/test}/use_animation_controller_test.dart (100%) rename {test => packages/flutter_hooks/test}/use_animation_test.dart (100%) rename {test => packages/flutter_hooks/test}/use_context_test.dart (100%) rename {test => packages/flutter_hooks/test}/use_effect_test.dart (100%) rename {test => packages/flutter_hooks/test}/use_focus_node_test.dart (100%) rename {test => packages/flutter_hooks/test}/use_future_test.dart (100%) rename {test => packages/flutter_hooks/test}/use_listenable_test.dart (100%) rename {test => packages/flutter_hooks/test}/use_page_controller_test.dart (100%) rename {test => packages/flutter_hooks/test}/use_previous_test.dart (100%) rename {test => packages/flutter_hooks/test}/use_reassemble_test.dart (100%) rename {test => packages/flutter_hooks/test}/use_reducer_test.dart (100%) rename {test => packages/flutter_hooks/test}/use_scroll_controller_test.dart (100%) rename {test => packages/flutter_hooks/test}/use_state_test.dart (100%) rename {test => packages/flutter_hooks/test}/use_stream_controller_test.dart (100%) rename {test => packages/flutter_hooks/test}/use_stream_test.dart (100%) rename {test => packages/flutter_hooks/test}/use_tab_controller_test.dart (100%) rename {test => packages/flutter_hooks/test}/use_text_editing_controller_test.dart (100%) rename {test => packages/flutter_hooks/test}/use_ticker_provider_test.dart (100%) rename {test => packages/flutter_hooks/test}/use_value_changed_test.dart (100%) rename {test => packages/flutter_hooks/test}/use_value_listenable_test.dart (100%) rename {test => packages/flutter_hooks/test}/use_value_notifier_test.dart (100%) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 41f26784..7229edb4 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -13,6 +13,8 @@ jobs: strategy: matrix: + package: + - flutter_hooks channel: - dev # - stable @@ -25,19 +27,20 @@ jobs: - name: Install dependencies run: flutter pub get - working-directory: ${{ matrix.package }} + working-directory: packages/${{ matrix.package }} - name: Check format run: flutter format --set-exit-if-changed . - working-directory: ${{ matrix.package }} + working-directory: packages/${{ matrix.package }} - name: Analyze run: flutter analyze - working-directory: ${{ matrix.package }} + working-directory: packages/${{ matrix.package }} - name: Run tests run: flutter test --coverage - working-directory: ${{ matrix.package }} + working-directory: packages/${{ matrix.package }} - name: Upload coverage to codecov run: curl -s https://codecov.io/bash | bash + working-directory: packages/${{ matrix.package }} diff --git a/README.md b/README.md index 29d6fefd..c010e92b 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ -[English](https://github.com/rrousselGit/flutter_hooks/blob/master/README.md) | [Português](https://github.com/rrousselGit/flutter_hooks/blob/master/resources/translations/pt_br/README.md) +[English](https://github.com/rrousselGit/flutter_hooks/blob/master/README.md) | [Português](https://github.com/rrousselGit/flutter_hooks/blob/master/packages/flutter_hooks/resources/translations/pt_br/README.md) [![Build](https://github.com/rrousselGit/flutter_hooks/workflows/Build/badge.svg)](https://github.com/rrousselGit/flutter_hooks/actions?query=workflow%3ABuild) [![codecov](https://codecov.io/gh/rrousselGit/flutter_hooks/branch/master/graph/badge.svg)](https://codecov.io/gh/rrousselGit/flutter_hooks) [![pub package](https://img.shields.io/pub/v/flutter_hooks.svg)](https://pub.dartlang.org/packages/flutter_hooks) [![pub package](https://img.shields.io/badge/Awesome-Flutter-blue.svg?longCache=true&style=flat-square)](https://github.com/Solido/awesome-flutter) - + # Flutter Hooks diff --git a/.gitignore b/packages/flutter_hooks/.gitignore similarity index 100% rename from .gitignore rename to packages/flutter_hooks/.gitignore diff --git a/CHANGELOG.md b/packages/flutter_hooks/CHANGELOG.md similarity index 100% rename from CHANGELOG.md rename to packages/flutter_hooks/CHANGELOG.md diff --git a/packages/flutter_hooks/LICENSE b/packages/flutter_hooks/LICENSE new file mode 120000 index 00000000..30cff740 --- /dev/null +++ b/packages/flutter_hooks/LICENSE @@ -0,0 +1 @@ +../../LICENSE \ No newline at end of file diff --git a/packages/flutter_hooks/README.md b/packages/flutter_hooks/README.md new file mode 120000 index 00000000..fe840054 --- /dev/null +++ b/packages/flutter_hooks/README.md @@ -0,0 +1 @@ +../../README.md \ No newline at end of file diff --git a/all_lint_rules.yaml b/packages/flutter_hooks/all_lint_rules.yaml similarity index 100% rename from all_lint_rules.yaml rename to packages/flutter_hooks/all_lint_rules.yaml diff --git a/analysis_options.yaml b/packages/flutter_hooks/analysis_options.yaml similarity index 100% rename from analysis_options.yaml rename to packages/flutter_hooks/analysis_options.yaml diff --git a/example/.gitignore b/packages/flutter_hooks/example/.gitignore similarity index 100% rename from example/.gitignore rename to packages/flutter_hooks/example/.gitignore diff --git a/example/.metadata b/packages/flutter_hooks/example/.metadata similarity index 100% rename from example/.metadata rename to packages/flutter_hooks/example/.metadata diff --git a/example/README.md b/packages/flutter_hooks/example/README.md similarity index 100% rename from example/README.md rename to packages/flutter_hooks/example/README.md diff --git a/example/analysis_options.yaml b/packages/flutter_hooks/example/analysis_options.yaml similarity index 100% rename from example/analysis_options.yaml rename to packages/flutter_hooks/example/analysis_options.yaml diff --git a/example/lib/custom_hook_function.dart b/packages/flutter_hooks/example/lib/custom_hook_function.dart similarity index 100% rename from example/lib/custom_hook_function.dart rename to packages/flutter_hooks/example/lib/custom_hook_function.dart diff --git a/example/lib/main.dart b/packages/flutter_hooks/example/lib/main.dart similarity index 100% rename from example/lib/main.dart rename to packages/flutter_hooks/example/lib/main.dart diff --git a/example/lib/star_wars/README.md b/packages/flutter_hooks/example/lib/star_wars/README.md similarity index 100% rename from example/lib/star_wars/README.md rename to packages/flutter_hooks/example/lib/star_wars/README.md diff --git a/example/lib/star_wars/models.dart b/packages/flutter_hooks/example/lib/star_wars/models.dart similarity index 100% rename from example/lib/star_wars/models.dart rename to packages/flutter_hooks/example/lib/star_wars/models.dart diff --git a/example/lib/star_wars/models.g.dart b/packages/flutter_hooks/example/lib/star_wars/models.g.dart similarity index 100% rename from example/lib/star_wars/models.g.dart rename to packages/flutter_hooks/example/lib/star_wars/models.g.dart diff --git a/example/lib/star_wars/planet_screen.dart b/packages/flutter_hooks/example/lib/star_wars/planet_screen.dart similarity index 100% rename from example/lib/star_wars/planet_screen.dart rename to packages/flutter_hooks/example/lib/star_wars/planet_screen.dart diff --git a/example/lib/star_wars/redux.dart b/packages/flutter_hooks/example/lib/star_wars/redux.dart similarity index 100% rename from example/lib/star_wars/redux.dart rename to packages/flutter_hooks/example/lib/star_wars/redux.dart diff --git a/example/lib/star_wars/redux.g.dart b/packages/flutter_hooks/example/lib/star_wars/redux.g.dart similarity index 100% rename from example/lib/star_wars/redux.g.dart rename to packages/flutter_hooks/example/lib/star_wars/redux.g.dart diff --git a/example/lib/star_wars/star_wars_api.dart b/packages/flutter_hooks/example/lib/star_wars/star_wars_api.dart similarity index 100% rename from example/lib/star_wars/star_wars_api.dart rename to packages/flutter_hooks/example/lib/star_wars/star_wars_api.dart diff --git a/example/lib/use_effect.dart b/packages/flutter_hooks/example/lib/use_effect.dart similarity index 100% rename from example/lib/use_effect.dart rename to packages/flutter_hooks/example/lib/use_effect.dart diff --git a/example/lib/use_reducer.dart b/packages/flutter_hooks/example/lib/use_reducer.dart similarity index 97% rename from example/lib/use_reducer.dart rename to packages/flutter_hooks/example/lib/use_reducer.dart index 205419b4..e365f0c4 100644 --- a/example/lib/use_reducer.dart +++ b/packages/flutter_hooks/example/lib/use_reducer.dart @@ -1,58 +1,58 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; - -/// This example emulates the basic Counter app generated by the -/// `flutter create` command to demonstrates the `useReducer` hook. -/// -/// First, instead of a StatefulWidget, use a HookWidget instead! - -@immutable -class State { - const State({this.counter = 0}); - final int counter; -} - -// Create the actions you wish to dispatch to the reducer -class IncrementCounter { - IncrementCounter({this.counter}); - int counter; -} - -class UseReducerExample extends HookWidget { - @override - Widget build(BuildContext context) { - // Create the reducer function that will handle the actions you dispatch - State _reducer(State state, IncrementCounter action) { - if (action is IncrementCounter) { - return State(counter: state.counter + action.counter); - } - return state; - } - - // Next, invoke the `useReducer` function with the reducer function and initial state to create a - // `_store` variable that contains the current state and dispatch. Whenever the value is - // changed, this Widget will be rebuilt! - final _store = useReducer( - _reducer, - initialState: const State(), - initialAction: null, - ); - - return Scaffold( - appBar: AppBar( - title: const Text('useState example'), - ), - body: Center( - // Read the current value from the counter - child: Text('Button tapped ${_store.state.counter} times'), - ), - floatingActionButton: FloatingActionButton( - // When the button is pressed, dispatch the Action you wish to trigger! This - // will trigger a rebuild, displaying the latest value in the Text - // Widget above! - onPressed: () => _store.dispatch(IncrementCounter(counter: 1)), - child: const Icon(Icons.add), - ), - ); - } -} +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; + +/// This example emulates the basic Counter app generated by the +/// `flutter create` command to demonstrates the `useReducer` hook. +/// +/// First, instead of a StatefulWidget, use a HookWidget instead! + +@immutable +class State { + const State({this.counter = 0}); + final int counter; +} + +// Create the actions you wish to dispatch to the reducer +class IncrementCounter { + IncrementCounter({this.counter}); + int counter; +} + +class UseReducerExample extends HookWidget { + @override + Widget build(BuildContext context) { + // Create the reducer function that will handle the actions you dispatch + State _reducer(State state, IncrementCounter action) { + if (action is IncrementCounter) { + return State(counter: state.counter + action.counter); + } + return state; + } + + // Next, invoke the `useReducer` function with the reducer function and initial state to create a + // `_store` variable that contains the current state and dispatch. Whenever the value is + // changed, this Widget will be rebuilt! + final _store = useReducer( + _reducer, + initialState: const State(), + initialAction: null, + ); + + return Scaffold( + appBar: AppBar( + title: const Text('useState example'), + ), + body: Center( + // Read the current value from the counter + child: Text('Button tapped ${_store.state.counter} times'), + ), + floatingActionButton: FloatingActionButton( + // When the button is pressed, dispatch the Action you wish to trigger! This + // will trigger a rebuild, displaying the latest value in the Text + // Widget above! + onPressed: () => _store.dispatch(IncrementCounter(counter: 1)), + child: const Icon(Icons.add), + ), + ); + } +} diff --git a/example/lib/use_state.dart b/packages/flutter_hooks/example/lib/use_state.dart similarity index 100% rename from example/lib/use_state.dart rename to packages/flutter_hooks/example/lib/use_state.dart diff --git a/example/lib/use_stream.dart b/packages/flutter_hooks/example/lib/use_stream.dart similarity index 100% rename from example/lib/use_stream.dart rename to packages/flutter_hooks/example/lib/use_stream.dart diff --git a/example/pubspec.yaml b/packages/flutter_hooks/example/pubspec.yaml similarity index 100% rename from example/pubspec.yaml rename to packages/flutter_hooks/example/pubspec.yaml diff --git a/flutter-hook.svg b/packages/flutter_hooks/flutter-hook.svg similarity index 100% rename from flutter-hook.svg rename to packages/flutter_hooks/flutter-hook.svg diff --git a/lib/flutter_hooks.dart b/packages/flutter_hooks/lib/flutter_hooks.dart similarity index 100% rename from lib/flutter_hooks.dart rename to packages/flutter_hooks/lib/flutter_hooks.dart diff --git a/lib/src/animation.dart b/packages/flutter_hooks/lib/src/animation.dart similarity index 100% rename from lib/src/animation.dart rename to packages/flutter_hooks/lib/src/animation.dart diff --git a/lib/src/async.dart b/packages/flutter_hooks/lib/src/async.dart similarity index 100% rename from lib/src/async.dart rename to packages/flutter_hooks/lib/src/async.dart diff --git a/lib/src/focus.dart b/packages/flutter_hooks/lib/src/focus.dart similarity index 100% rename from lib/src/focus.dart rename to packages/flutter_hooks/lib/src/focus.dart diff --git a/lib/src/framework.dart b/packages/flutter_hooks/lib/src/framework.dart similarity index 100% rename from lib/src/framework.dart rename to packages/flutter_hooks/lib/src/framework.dart diff --git a/lib/src/hooks.dart b/packages/flutter_hooks/lib/src/hooks.dart similarity index 100% rename from lib/src/hooks.dart rename to packages/flutter_hooks/lib/src/hooks.dart diff --git a/lib/src/listenable.dart b/packages/flutter_hooks/lib/src/listenable.dart similarity index 100% rename from lib/src/listenable.dart rename to packages/flutter_hooks/lib/src/listenable.dart diff --git a/lib/src/misc.dart b/packages/flutter_hooks/lib/src/misc.dart similarity index 100% rename from lib/src/misc.dart rename to packages/flutter_hooks/lib/src/misc.dart diff --git a/lib/src/page_controller.dart b/packages/flutter_hooks/lib/src/page_controller.dart similarity index 100% rename from lib/src/page_controller.dart rename to packages/flutter_hooks/lib/src/page_controller.dart diff --git a/lib/src/primitives.dart b/packages/flutter_hooks/lib/src/primitives.dart similarity index 100% rename from lib/src/primitives.dart rename to packages/flutter_hooks/lib/src/primitives.dart diff --git a/lib/src/scroll_controller.dart b/packages/flutter_hooks/lib/src/scroll_controller.dart similarity index 100% rename from lib/src/scroll_controller.dart rename to packages/flutter_hooks/lib/src/scroll_controller.dart diff --git a/lib/src/tab_controller.dart b/packages/flutter_hooks/lib/src/tab_controller.dart similarity index 100% rename from lib/src/tab_controller.dart rename to packages/flutter_hooks/lib/src/tab_controller.dart diff --git a/lib/src/text_controller.dart b/packages/flutter_hooks/lib/src/text_controller.dart similarity index 100% rename from lib/src/text_controller.dart rename to packages/flutter_hooks/lib/src/text_controller.dart diff --git a/pubspec.yaml b/packages/flutter_hooks/pubspec.yaml similarity index 100% rename from pubspec.yaml rename to packages/flutter_hooks/pubspec.yaml diff --git a/resources/translations/pt_br/README.md b/packages/flutter_hooks/resources/translations/pt_br/README.md similarity index 98% rename from resources/translations/pt_br/README.md rename to packages/flutter_hooks/resources/translations/pt_br/README.md index 1f6af085..4d615e38 100644 --- a/resources/translations/pt_br/README.md +++ b/packages/flutter_hooks/resources/translations/pt_br/README.md @@ -1,8 +1,8 @@ -[English](https://github.com/rrousselGit/flutter_hooks/blob/master/README.md) | [Português](https://github.com/rrousselGit/flutter_hooks/blob/master/resources/translations/pt_br/README.md) +[English](https://github.com/rrousselGit/flutter_hooks/blob/master/README.md) | [Português](https://github.com/rrousselGit/flutter_hooks/blob/master/packages/flutter_hooks/resources/translations/pt_br/README.md) [![Build Status](https://travis-ci.org/rrousselGit/flutter_hooks.svg?branch=master)](https://travis-ci.org/rrousselGit/flutter_hooks) [![codecov](https://codecov.io/gh/rrousselGit/flutter_hooks/branch/master/graph/badge.svg)](https://codecov.io/gh/rrousselGit/flutter_hooks) [![pub package](https://img.shields.io/pub/v/flutter_hooks.svg)](https://pub.dartlang.org/packages/flutter_hooks) [![pub package](https://img.shields.io/badge/Awesome-Flutter-blue.svg?longCache=true&style=flat-square)](https://github.com/Solido/awesome-flutter) - + # Flutter Hooks diff --git a/test/hook_builder_test.dart b/packages/flutter_hooks/test/hook_builder_test.dart similarity index 100% rename from test/hook_builder_test.dart rename to packages/flutter_hooks/test/hook_builder_test.dart diff --git a/test/hook_widget_test.dart b/packages/flutter_hooks/test/hook_widget_test.dart similarity index 100% rename from test/hook_widget_test.dart rename to packages/flutter_hooks/test/hook_widget_test.dart diff --git a/test/is_mounted_test.dart b/packages/flutter_hooks/test/is_mounted_test.dart similarity index 100% rename from test/is_mounted_test.dart rename to packages/flutter_hooks/test/is_mounted_test.dart diff --git a/test/memoized_test.dart b/packages/flutter_hooks/test/memoized_test.dart similarity index 100% rename from test/memoized_test.dart rename to packages/flutter_hooks/test/memoized_test.dart diff --git a/test/mock.dart b/packages/flutter_hooks/test/mock.dart similarity index 100% rename from test/mock.dart rename to packages/flutter_hooks/test/mock.dart diff --git a/test/pre_build_abort_test.dart b/packages/flutter_hooks/test/pre_build_abort_test.dart similarity index 100% rename from test/pre_build_abort_test.dart rename to packages/flutter_hooks/test/pre_build_abort_test.dart diff --git a/test/use_animation_controller_test.dart b/packages/flutter_hooks/test/use_animation_controller_test.dart similarity index 100% rename from test/use_animation_controller_test.dart rename to packages/flutter_hooks/test/use_animation_controller_test.dart diff --git a/test/use_animation_test.dart b/packages/flutter_hooks/test/use_animation_test.dart similarity index 100% rename from test/use_animation_test.dart rename to packages/flutter_hooks/test/use_animation_test.dart diff --git a/test/use_context_test.dart b/packages/flutter_hooks/test/use_context_test.dart similarity index 100% rename from test/use_context_test.dart rename to packages/flutter_hooks/test/use_context_test.dart diff --git a/test/use_effect_test.dart b/packages/flutter_hooks/test/use_effect_test.dart similarity index 100% rename from test/use_effect_test.dart rename to packages/flutter_hooks/test/use_effect_test.dart diff --git a/test/use_focus_node_test.dart b/packages/flutter_hooks/test/use_focus_node_test.dart similarity index 100% rename from test/use_focus_node_test.dart rename to packages/flutter_hooks/test/use_focus_node_test.dart diff --git a/test/use_future_test.dart b/packages/flutter_hooks/test/use_future_test.dart similarity index 100% rename from test/use_future_test.dart rename to packages/flutter_hooks/test/use_future_test.dart diff --git a/test/use_listenable_test.dart b/packages/flutter_hooks/test/use_listenable_test.dart similarity index 100% rename from test/use_listenable_test.dart rename to packages/flutter_hooks/test/use_listenable_test.dart diff --git a/test/use_page_controller_test.dart b/packages/flutter_hooks/test/use_page_controller_test.dart similarity index 100% rename from test/use_page_controller_test.dart rename to packages/flutter_hooks/test/use_page_controller_test.dart diff --git a/test/use_previous_test.dart b/packages/flutter_hooks/test/use_previous_test.dart similarity index 100% rename from test/use_previous_test.dart rename to packages/flutter_hooks/test/use_previous_test.dart diff --git a/test/use_reassemble_test.dart b/packages/flutter_hooks/test/use_reassemble_test.dart similarity index 100% rename from test/use_reassemble_test.dart rename to packages/flutter_hooks/test/use_reassemble_test.dart diff --git a/test/use_reducer_test.dart b/packages/flutter_hooks/test/use_reducer_test.dart similarity index 100% rename from test/use_reducer_test.dart rename to packages/flutter_hooks/test/use_reducer_test.dart diff --git a/test/use_scroll_controller_test.dart b/packages/flutter_hooks/test/use_scroll_controller_test.dart similarity index 100% rename from test/use_scroll_controller_test.dart rename to packages/flutter_hooks/test/use_scroll_controller_test.dart diff --git a/test/use_state_test.dart b/packages/flutter_hooks/test/use_state_test.dart similarity index 100% rename from test/use_state_test.dart rename to packages/flutter_hooks/test/use_state_test.dart diff --git a/test/use_stream_controller_test.dart b/packages/flutter_hooks/test/use_stream_controller_test.dart similarity index 100% rename from test/use_stream_controller_test.dart rename to packages/flutter_hooks/test/use_stream_controller_test.dart diff --git a/test/use_stream_test.dart b/packages/flutter_hooks/test/use_stream_test.dart similarity index 100% rename from test/use_stream_test.dart rename to packages/flutter_hooks/test/use_stream_test.dart diff --git a/test/use_tab_controller_test.dart b/packages/flutter_hooks/test/use_tab_controller_test.dart similarity index 100% rename from test/use_tab_controller_test.dart rename to packages/flutter_hooks/test/use_tab_controller_test.dart diff --git a/test/use_text_editing_controller_test.dart b/packages/flutter_hooks/test/use_text_editing_controller_test.dart similarity index 100% rename from test/use_text_editing_controller_test.dart rename to packages/flutter_hooks/test/use_text_editing_controller_test.dart diff --git a/test/use_ticker_provider_test.dart b/packages/flutter_hooks/test/use_ticker_provider_test.dart similarity index 100% rename from test/use_ticker_provider_test.dart rename to packages/flutter_hooks/test/use_ticker_provider_test.dart diff --git a/test/use_value_changed_test.dart b/packages/flutter_hooks/test/use_value_changed_test.dart similarity index 100% rename from test/use_value_changed_test.dart rename to packages/flutter_hooks/test/use_value_changed_test.dart diff --git a/test/use_value_listenable_test.dart b/packages/flutter_hooks/test/use_value_listenable_test.dart similarity index 100% rename from test/use_value_listenable_test.dart rename to packages/flutter_hooks/test/use_value_listenable_test.dart diff --git a/test/use_value_notifier_test.dart b/packages/flutter_hooks/test/use_value_notifier_test.dart similarity index 100% rename from test/use_value_notifier_test.dart rename to packages/flutter_hooks/test/use_value_notifier_test.dart From 239bd8961d897fbf3801cefa1b7b4b8d3024f413 Mon Sep 17 00:00:00 2001 From: Tyler Norbury Date: Tue, 4 May 2021 17:27:34 -0700 Subject: [PATCH 221/384] Doc fixes (#241) * Reference the two different setters, instead of just one * misc doc changes --- packages/flutter_hooks/lib/src/framework.dart | 10 +++++----- packages/flutter_hooks/lib/src/text_controller.dart | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/flutter_hooks/lib/src/framework.dart b/packages/flutter_hooks/lib/src/framework.dart index 96fecfb4..1795b2a7 100644 --- a/packages/flutter_hooks/lib/src/framework.dart +++ b/packages/flutter_hooks/lib/src/framework.dart @@ -58,7 +58,7 @@ R use(Hook hook) => Hook.use(hook); /// [Hook] is powerful tool to reuse [State] logic between multiple [Widget]. /// They are used to extract logic that depends on a [Widget] life-cycle (such as [HookState.dispose]). /// -/// While mixins are a good candidate too, they do not allow sharing values. A mixin cannot reasonnably +/// While mixins are a good candidate too, they do not allow sharing values. A mixin cannot reasonably /// define a variable, as this can lead to variable conflicts on bigger widgets. /// /// Hooks are designed so that they get the benefits of mixins, but are totally independent from each others. @@ -96,8 +96,8 @@ R use(Hook hook) => Hook.use(hook); /// } /// ``` /// -/// This is undesired because every single widget that want to use an [AnimationController] will have to -/// rewrite this extact piece of code. +/// This is undesired because every single widget that wants to use an [AnimationController] will have to +/// rewrite this exact piece of code. /// /// With hooks it is possible to extract that exact piece of code into a reusable one. /// @@ -114,7 +114,7 @@ R use(Hook hook) => Hook.use(hook); /// ``` /// /// This is visibly less code then before. But in this example, the `animationController` is still -/// guaranted to be disposed when the widget is removed from the tree. +/// guaranteed to be disposed when the widget is removed from the tree. /// /// In fact this has secondary bonus: `duration` is kept updated with the latest value. /// If we were to pass a variable as `duration` instead of a constant, then on value change the [AnimationController] will be updated. @@ -229,7 +229,7 @@ abstract class HookState> with Diagnosticable { @protected void dispose() {} - /// Called everytimes the [HookState] is requested + /// Called everytime the [HookState] is requested /// /// [build] is where an [HookState] may use other hooks. This restriction is made to ensure that hooks are unconditionally always requested @protected diff --git a/packages/flutter_hooks/lib/src/text_controller.dart b/packages/flutter_hooks/lib/src/text_controller.dart index 259a4214..1d5e52fb 100644 --- a/packages/flutter_hooks/lib/src/text_controller.dart +++ b/packages/flutter_hooks/lib/src/text_controller.dart @@ -38,7 +38,7 @@ class _TextEditingControllerHookCreator { /// Changing the text or initial value after the widget has been built has no /// effect whatsoever. To update the value in a callback, for instance after a /// button was pressed, use the [TextEditingController.text] or -/// [TextEditingController.text] setters. To have the [TextEditingController] +/// [TextEditingController.value] setters. To have the [TextEditingController] /// reflect changing values, you can use [useEffect]. This example will update /// the [TextEditingController.text] whenever a provided [ValueListenable] /// changes: From ffa3188ecfa77d2c6b2fb1cb5e3d631d2365ddb9 Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Thu, 13 May 2021 08:50:56 +0100 Subject: [PATCH 222/384] fix warnings --- packages/flutter_hooks/analysis_options.yaml | 3 --- packages/flutter_hooks/test/memoized_test.dart | 2 +- packages/flutter_hooks/test/use_focus_node_test.dart | 9 ++++++--- .../flutter_hooks/test/use_scroll_controller_test.dart | 2 +- packages/flutter_hooks/test/use_tab_controller_test.dart | 2 +- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/flutter_hooks/analysis_options.yaml b/packages/flutter_hooks/analysis_options.yaml index 40420f35..6607ce52 100644 --- a/packages/flutter_hooks/analysis_options.yaml +++ b/packages/flutter_hooks/analysis_options.yaml @@ -12,9 +12,6 @@ analyzer: # in this file included_file_warning: ignore - # Causes false positives (https://github.com/dart-lang/sdk/issues/41571 - top_level_function_literal_block: ignore - linter: rules: # Uninitianalized late variables are dangerous diff --git a/packages/flutter_hooks/test/memoized_test.dart b/packages/flutter_hooks/test/memoized_test.dart index 5ff48c85..1db2339a 100644 --- a/packages/flutter_hooks/test/memoized_test.dart +++ b/packages/flutter_hooks/test/memoized_test.dart @@ -1,5 +1,5 @@ -import 'package:flutter/widgets.dart'; import 'package:flutter/foundation.dart'; +import 'package:flutter/widgets.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'mock.dart'; diff --git a/packages/flutter_hooks/test/use_focus_node_test.dart b/packages/flutter_hooks/test/use_focus_node_test.dart index 20f3aa7d..420d2aba 100644 --- a/packages/flutter_hooks/test/use_focus_node_test.dart +++ b/packages/flutter_hooks/test/use_focus_node_test.dart @@ -81,7 +81,8 @@ void main() { }); testWidgets('has all the FocusNode parameters', (tester) async { - bool onKey(FocusNode node, RawKeyEvent event) => true; + KeyEventResult onKey(FocusNode node, RawKeyEvent event) => + KeyEventResult.ignored; late FocusNode focusNode; await tester.pumpWidget( @@ -105,8 +106,10 @@ void main() { }); testWidgets('handles parameter change', (tester) async { - bool onKey(FocusNode node, RawKeyEvent event) => true; - bool onKey2(FocusNode node, RawKeyEvent event) => true; + KeyEventResult onKey(FocusNode node, RawKeyEvent event) => + KeyEventResult.ignored; + KeyEventResult onKey2(FocusNode node, RawKeyEvent event) => + KeyEventResult.ignored; late FocusNode focusNode; await tester.pumpWidget( diff --git a/packages/flutter_hooks/test/use_scroll_controller_test.dart b/packages/flutter_hooks/test/use_scroll_controller_test.dart index 5657d2b5..4a3a34fa 100644 --- a/packages/flutter_hooks/test/use_scroll_controller_test.dart +++ b/packages/flutter_hooks/test/use_scroll_controller_test.dart @@ -1,9 +1,9 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; -import 'package:flutter_test/flutter_test.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'; diff --git a/packages/flutter_hooks/test/use_tab_controller_test.dart b/packages/flutter_hooks/test/use_tab_controller_test.dart index 83bc978f..b05a6348 100644 --- a/packages/flutter_hooks/test/use_tab_controller_test.dart +++ b/packages/flutter_hooks/test/use_tab_controller_test.dart @@ -1,9 +1,9 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; -import 'package:flutter_test/flutter_test.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'; From 0c5609ceda556e7883ee04c077f4c0254a58b4e6 Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Thu, 13 May 2021 08:53:12 +0100 Subject: [PATCH 223/384] Fix tests --- packages/flutter_hooks/test/mock.dart | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/flutter_hooks/test/mock.dart b/packages/flutter_hooks/test/mock.dart index 21f8bb98..69dc271e 100644 --- a/packages/flutter_hooks/test/mock.dart +++ b/packages/flutter_hooks/test/mock.dart @@ -112,7 +112,13 @@ class MockCreateState> extends Mock { MockCreateState(this.value); final T value; - T call() => value; + T call() { + return super.noSuchMethod( + Invocation.method(#call, []), + returnValue: value, + returnValueForMissingStub: value, + ) as T; + } } class MockBuild extends Mock { From ccde23d892329777441ec005f8d90ebe85b2f81b Mon Sep 17 00:00:00 2001 From: Goxiaoy Date: Thu, 13 May 2021 16:00:53 +0800 Subject: [PATCH 224/384] Pass the error StackTrace to useFuture/useStream (#238) --- packages/flutter_hooks/lib/src/async.dart | 21 +++++++++++++------ .../flutter_hooks/test/use_future_test.dart | 6 ++++-- .../flutter_hooks/test/use_stream_test.dart | 6 ++++-- 3 files changed, 23 insertions(+), 10 deletions(-) diff --git a/packages/flutter_hooks/lib/src/async.dart b/packages/flutter_hooks/lib/src/async.dart index bdb9974e..62215512 100644 --- a/packages/flutter_hooks/lib/src/async.dart +++ b/packages/flutter_hooks/lib/src/async.dart @@ -83,11 +83,15 @@ class _FutureStateHook extends HookState, _FutureHook> { _snapshot = AsyncSnapshot.withData(ConnectionState.done, data); }); } - }, onError: (dynamic error) { + // ignore: avoid_types_on_closure_parameters + }, onError: (Object error, StackTrace stackTrace) { if (_activeCallbackIdentity == callbackIdentity) { setState(() { _snapshot = AsyncSnapshot.withError( - ConnectionState.done, error as Object); + ConnectionState.done, + error, + stackTrace, + ); }); } }); @@ -186,9 +190,10 @@ class _StreamHookState extends HookState, _StreamHook> { setState(() { _summary = afterData(data); }); - }, onError: (dynamic error) { + // ignore: avoid_types_on_closure_parameters + }, onError: (Object error, StackTrace stackTrace) { setState(() { - _summary = afterError(error as Object); + _summary = afterError(error, stackTrace); }); }, onDone: () { setState(() { @@ -219,8 +224,12 @@ class _StreamHookState extends HookState, _StreamHook> { return AsyncSnapshot.withData(ConnectionState.active, data); } - AsyncSnapshot afterError(Object error) { - return AsyncSnapshot.withError(ConnectionState.active, error); + AsyncSnapshot afterError(Object error, StackTrace stackTrace) { + return AsyncSnapshot.withError( + ConnectionState.active, + error, + stackTrace, + ); } AsyncSnapshot afterDone(AsyncSnapshot current) => diff --git a/packages/flutter_hooks/test/use_future_test.dart b/packages/flutter_hooks/test/use_future_test.dart index b1f45969..38422b21 100644 --- a/packages/flutter_hooks/test/use_future_test.dart +++ b/packages/flutter_hooks/test/use_future_test.dart @@ -134,10 +134,12 @@ void main() { find.text( 'AsyncSnapshot(ConnectionState.waiting, null, null, null)'), findsOneWidget); - completer.completeError('bad'); + completer.completeError('bad', StackTrace.fromString('stackTrace')); await eventFiring(tester); expect( - find.text('AsyncSnapshot(ConnectionState.done, null, bad, )'), + find.text( + 'AsyncSnapshot(ConnectionState.done, null, bad, stackTrace)', + ), findsOneWidget); }); testWidgets('runs the builder using given initial data', (tester) async { diff --git a/packages/flutter_hooks/test/use_stream_test.dart b/packages/flutter_hooks/test/use_stream_test.dart index 8dd1ab2e..9e31b274 100644 --- a/packages/flutter_hooks/test/use_stream_test.dart +++ b/packages/flutter_hooks/test/use_stream_test.dart @@ -127,10 +127,12 @@ void main() { findsOneWidget); controller ..add('3') - ..addError('bad'); + ..addError('bad', StackTrace.fromString('stackTrace')); await eventFiring(tester); expect( - find.text('AsyncSnapshot(ConnectionState.active, null, bad, )'), + find.text( + 'AsyncSnapshot(ConnectionState.active, null, bad, stackTrace)', + ), findsOneWidget); controller.add('4'); await controller.close(); From ff98316a30486a49a52beea038f2d06cf273f667 Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Thu, 13 May 2021 09:02:00 +0100 Subject: [PATCH 225/384] Update changelog --- packages/flutter_hooks/CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/flutter_hooks/CHANGELOG.md b/packages/flutter_hooks/CHANGELOG.md index 66b06179..f6c9eeca 100644 --- a/packages/flutter_hooks/CHANGELOG.md +++ b/packages/flutter_hooks/CHANGELOG.md @@ -1,3 +1,7 @@ +## [Unreleased] + +- `useFuture`/`useStream`'s `AsynsSnapshot` now correctly expose the StackTrace when there is an error. + ## 0.16.0 Stable null-safety release From d0b8d0490adb73516dd9ad9537cdb7576188867d Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Thu, 13 May 2021 09:18:50 +0100 Subject: [PATCH 226/384] 0.17.0 --- packages/flutter_hooks/CHANGELOG.md | 2 +- packages/flutter_hooks/pubspec.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/flutter_hooks/CHANGELOG.md b/packages/flutter_hooks/CHANGELOG.md index f6c9eeca..981d8a74 100644 --- a/packages/flutter_hooks/CHANGELOG.md +++ b/packages/flutter_hooks/CHANGELOG.md @@ -1,4 +1,4 @@ -## [Unreleased] +## 0.17.0 - `useFuture`/`useStream`'s `AsynsSnapshot` now correctly expose the StackTrace when there is an error. diff --git a/packages/flutter_hooks/pubspec.yaml b/packages/flutter_hooks/pubspec.yaml index d451ac17..d7e7ab85 100644 --- a/packages/flutter_hooks/pubspec.yaml +++ b/packages/flutter_hooks/pubspec.yaml @@ -1,7 +1,7 @@ name: flutter_hooks description: A flutter implementation of React hooks. It adds a new kind of widget with enhanced code reuse. homepage: https://github.com/rrousselGit/flutter_hooks -version: 0.16.0 +version: 0.17.0 environment: sdk: ">=2.12.0-0 <3.0.0" From b68f52115961d9156a5b6042b1ed9bb97891d350 Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Thu, 13 May 2021 09:20:52 +0100 Subject: [PATCH 227/384] Fix useSingleTickerProvider error message fixes #223 --- packages/flutter_hooks/lib/src/animation.dart | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/flutter_hooks/lib/src/animation.dart b/packages/flutter_hooks/lib/src/animation.dart index 42dd59ee..dcb50718 100644 --- a/packages/flutter_hooks/lib/src/animation.dart +++ b/packages/flutter_hooks/lib/src/animation.dart @@ -170,10 +170,9 @@ class _TickerProviderHookState } throw FlutterError( '${context.widget.runtimeType} attempted to use a useSingleTickerProvider multiple times.\n' - 'A SingleTickerProviderStateMixin can only be used as a TickerProvider once. If a ' - 'TickerProvider is used for multiple AnimationController objects, or if it is passed to other ' - 'objects and those objects might use it more than one time in total, then instead of ' - 'using useSingleTickerProvider, use a regular useTickerProvider.'); + 'A SingleTickerProviderStateMixin can only be used as a TickerProvider once. ' + 'If you need multiple Ticker, consider using useSingleTickerProvider multiple times ' + 'to create as many Tickers as needed.'); }(), ''); return _ticker = Ticker(onTick, debugLabel: 'created by $context'); } From 5b244693a46d74ec0c3389c04c7d661e2cc807f9 Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Thu, 13 May 2021 09:44:36 +0100 Subject: [PATCH 228/384] add useRef/useCallback --- README.md | 4 +- packages/flutter_hooks/CHANGELOG.md | 1 + .../flutter_hooks/lib/src/primitives.dart | 43 +++++++++++++ .../flutter_hooks/test/memoized_test.dart | 62 +++++++++++++++++++ 4 files changed, 109 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index c010e92b..123fe715 100644 --- a/README.md +++ b/README.md @@ -299,6 +299,8 @@ A set of low-level hooks that interacts with the different life-cycles of a widg | [useEffect](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useEffect.html) | Useful for side-effects and optionally canceling them. | | [useState](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useState.html) | Create variable and subscribes to it. | | [useMemoized](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useMemoized.html) | Cache the instance of a complex object. | +| [useRef](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useRef.html) | Creates an object that contains a single mutable property. | +| [useCallback](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useCallback.html) | Cache a function insteance. | | [useContext](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useContext.html) | Obtain the `BuildContext` of the building `HookWidget`. | | [useValueChanged](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useValueChanged.html) | Watches a value and calls a callback whenever the value changed. | @@ -343,7 +345,7 @@ A series of hooks with no particular theme. | [useFocusNode](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useFocusNode.html) | Create a `FocusNode` | | [useTabController](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useTabController.html) | Creates and disposes a `TabController`. | | [useScrollController](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useScrollController.html) | Creates and disposes a `ScrollController`. | -| [usePageController](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/usePageController.html) | Creates and disposes a `PageController`. | +| [usePageController](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/usePageController.html) | Creates and disposes a `PageController`. | | [useIsMounted](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useIsMounted.html) | An equivalent to `State.mounted` for hooks | ## Contributions diff --git a/packages/flutter_hooks/CHANGELOG.md b/packages/flutter_hooks/CHANGELOG.md index 981d8a74..890cca2a 100644 --- a/packages/flutter_hooks/CHANGELOG.md +++ b/packages/flutter_hooks/CHANGELOG.md @@ -1,6 +1,7 @@ ## 0.17.0 - `useFuture`/`useStream`'s `AsynsSnapshot` now correctly expose the StackTrace when there is an error. +- added `useRef` and `useCallback`, similar to the React equivalents. ## 0.16.0 diff --git a/packages/flutter_hooks/lib/src/primitives.dart b/packages/flutter_hooks/lib/src/primitives.dart index f9cc77b5..699a08bd 100644 --- a/packages/flutter_hooks/lib/src/primitives.dart +++ b/packages/flutter_hooks/lib/src/primitives.dart @@ -1,5 +1,48 @@ part of 'hooks.dart'; +/// A class that stores a single value; +/// +/// It is typically created by [useRef]. +class ObjectRef { + /// A mutable property that will be preserved accross rebuilds. + /// + /// Updating this property will not cause widgets to rebuild. + T? value; +} + +/// Creates an object that contains a single mutable property. +/// +/// Mutating the object's property has no effect. +/// This is useful for sharing state accross `build` calls, without causing +/// unnecessary rebuilds. +ObjectRef useRef() { + return useMemoized(() => ObjectRef()); +} + +/// Cache a function accross rebuilds based on a list of keys. +/// +/// This is syntax sugar for [useMemoized], such that instead of: +/// +/// ```dart +/// final cachedFunction = useMemoized(() => () { +/// print('doSomething'); +/// }, [key]); +/// ``` +/// +/// we can directly do: +/// +/// ```dart +/// final cachedFunction = useCallback(() { +/// print('doSomething'); +/// }, [key]); +/// ``` +T useCallback( + T callback, + List keys, +) { + return useMemoized(() => callback, keys); +} + /// Cache the instance of a complex object. /// /// [useMemoized] will immediately call [valueBuilder] on first call and store its result. diff --git a/packages/flutter_hooks/test/memoized_test.dart b/packages/flutter_hooks/test/memoized_test.dart index 1db2339a..906c46c0 100644 --- a/packages/flutter_hooks/test/memoized_test.dart +++ b/packages/flutter_hooks/test/memoized_test.dart @@ -11,6 +11,68 @@ void main() { reset(valueBuilder); }); + testWidgets('useRef', (tester) async { + late ObjectRef ref; + + await tester.pumpWidget( + HookBuilder(builder: (context) { + ref = useRef(); + return Container(); + }), + ); + + expect(ref.value, null); + ref.value = 42; + + late ObjectRef ref2; + + await tester.pumpWidget( + HookBuilder(builder: (context) { + ref2 = useRef(); + return Container(); + }), + ); + + expect(ref2, ref); + expect(ref2.value, 42); + }); + + testWidgets('useCallback', (tester) async { + late int Function() fn; + + await tester.pumpWidget( + HookBuilder(builder: (context) { + fn = useCallback(() => 42, []); + return Container(); + }), + ); + + expect(fn(), 42); + + late int Function() fn2; + + await tester.pumpWidget( + HookBuilder(builder: (context) { + fn2 = useCallback(() => 42, []); + return Container(); + }), + ); + + expect(fn2, fn); + + late int Function() fn3; + + await tester.pumpWidget( + HookBuilder(builder: (context) { + fn3 = useCallback(() => 21, [42]); + return Container(); + }), + ); + + expect(fn3, isNot(fn)); + expect(fn3(), 21); + }); + testWidgets('memoized without parameter calls valueBuilder once', (tester) async { late int result; From e991316d5a3bc1e2f4b8e2419dbb195ea4e45d09 Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Thu, 13 May 2021 09:57:52 +0100 Subject: [PATCH 229/384] made initialData of useStream and useFuture optional fixes #229 --- packages/flutter_hooks/lib/src/async.dart | 56 +++--- .../flutter_hooks/test/use_future_test.dart | 165 +++++++++++------- .../flutter_hooks/test/use_stream_test.dart | 78 ++++++--- 3 files changed, 184 insertions(+), 115 deletions(-) diff --git a/packages/flutter_hooks/lib/src/async.dart b/packages/flutter_hooks/lib/src/async.dart index 62215512..71e00b79 100644 --- a/packages/flutter_hooks/lib/src/async.dart +++ b/packages/flutter_hooks/lib/src/async.dart @@ -8,9 +8,9 @@ part of 'hooks.dart'; /// See also: /// * [Future], the listened object. /// * [useStream], similar to [useFuture] but for [Stream]. -AsyncSnapshot useFuture( +AsyncSnapshot useFuture( Future? future, { - required T initialData, + T? initialData, bool preserveState = true, }) { return use( @@ -22,7 +22,7 @@ AsyncSnapshot useFuture( ); } -class _FutureHook extends Hook> { +class _FutureHook extends Hook> { const _FutureHook( this.future, { required this.initialData, @@ -31,19 +31,19 @@ class _FutureHook extends Hook> { final Future? future; final bool preserveState; - final T initialData; + final T? initialData; @override _FutureStateHook createState() => _FutureStateHook(); } -class _FutureStateHook extends HookState, _FutureHook> { +class _FutureStateHook extends HookState, _FutureHook> { /// 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 _snapshot = - AsyncSnapshot.withData(ConnectionState.none, hook.initialData); + late AsyncSnapshot _snapshot = + AsyncSnapshot.withData(ConnectionState.none, hook.initialData); @override void initHook() { @@ -60,8 +60,8 @@ class _FutureStateHook extends HookState, _FutureHook> { if (hook.preserveState) { _snapshot = _snapshot.inState(ConnectionState.none); } else { - _snapshot = - AsyncSnapshot.withData(ConnectionState.none, hook.initialData); + _snapshot = AsyncSnapshot.withData( + ConnectionState.none, hook.initialData); } } _subscribe(); @@ -80,14 +80,14 @@ class _FutureStateHook extends HookState, _FutureHook> { hook.future!.then((data) { if (_activeCallbackIdentity == callbackIdentity) { setState(() { - _snapshot = AsyncSnapshot.withData(ConnectionState.done, data); + _snapshot = AsyncSnapshot.withData(ConnectionState.done, data); }); } // ignore: avoid_types_on_closure_parameters }, onError: (Object error, StackTrace stackTrace) { if (_activeCallbackIdentity == callbackIdentity) { setState(() { - _snapshot = AsyncSnapshot.withError( + _snapshot = AsyncSnapshot.withError( ConnectionState.done, error, stackTrace, @@ -104,7 +104,7 @@ class _FutureStateHook extends HookState, _FutureHook> { } @override - AsyncSnapshot build(BuildContext context) { + AsyncSnapshot build(BuildContext context) { return _snapshot; } @@ -123,9 +123,9 @@ class _FutureStateHook extends HookState, _FutureHook> { /// See also: /// * [Stream], the object listened. /// * [useFuture], similar to [useStream] but for [Future]. -AsyncSnapshot useStream( +AsyncSnapshot useStream( Stream? stream, { - required T initialData, + T? initialData, bool preserveState = true, }) { return use( @@ -137,7 +137,7 @@ AsyncSnapshot useStream( ); } -class _StreamHook extends Hook> { +class _StreamHook extends Hook> { const _StreamHook( this.stream, { required this.initialData, @@ -145,7 +145,7 @@ class _StreamHook extends Hook> { }); final Stream? stream; - final T initialData; + final T? initialData; final bool preserveState; @override @@ -153,9 +153,9 @@ class _StreamHook extends Hook> { } /// a clone of [StreamBuilderBase] implementation -class _StreamHookState extends HookState, _StreamHook> { +class _StreamHookState extends HookState, _StreamHook> { StreamSubscription? _subscription; - late AsyncSnapshot _summary = initial; + late AsyncSnapshot _summary = initial; @override void initHook() { @@ -210,32 +210,32 @@ class _StreamHookState extends HookState, _StreamHook> { } @override - AsyncSnapshot build(BuildContext context) { + AsyncSnapshot build(BuildContext context) { return _summary; } - AsyncSnapshot get initial => - AsyncSnapshot.withData(ConnectionState.none, hook.initialData); + AsyncSnapshot get initial => + AsyncSnapshot.withData(ConnectionState.none, hook.initialData); - AsyncSnapshot afterConnected(AsyncSnapshot current) => + AsyncSnapshot afterConnected(AsyncSnapshot current) => current.inState(ConnectionState.waiting); - AsyncSnapshot afterData(T data) { - return AsyncSnapshot.withData(ConnectionState.active, data); + AsyncSnapshot afterData(T data) { + return AsyncSnapshot.withData(ConnectionState.active, data); } - AsyncSnapshot afterError(Object error, StackTrace stackTrace) { - return AsyncSnapshot.withError( + AsyncSnapshot afterError(Object error, StackTrace stackTrace) { + return AsyncSnapshot.withError( ConnectionState.active, error, stackTrace, ); } - AsyncSnapshot afterDone(AsyncSnapshot current) => + AsyncSnapshot afterDone(AsyncSnapshot current) => current.inState(ConnectionState.done); - AsyncSnapshot afterDisconnected(AsyncSnapshot current) => + AsyncSnapshot afterDisconnected(AsyncSnapshot current) => current.inState(ConnectionState.none); @override diff --git a/packages/flutter_hooks/test/use_future_test.dart b/packages/flutter_hooks/test/use_future_test.dart index 38422b21..9af667a6 100644 --- a/packages/flutter_hooks/test/use_future_test.dart +++ b/packages/flutter_hooks/test/use_future_test.dart @@ -12,7 +12,7 @@ void main() { late AsyncSnapshot value; Widget Function(BuildContext) builder(Future stream) { return (context) { - value = useFuture(stream, initialData: null); + value = useFuture(stream); return Container(); }; } @@ -50,7 +50,7 @@ void main() { .toStringDeep(), equalsIgnoringHashCodes( 'HookBuilder\n' - ' │ useFuture: AsyncSnapshot(ConnectionState.done, 42, null,\n' + ' │ useFuture: AsyncSnapshot(ConnectionState.done, 42, null,\n' ' │ null)\n' ' └SizedBox(renderObject: RenderConstrainedBox#00000)\n', ), @@ -62,7 +62,7 @@ void main() { late AsyncSnapshot value; Widget Function(BuildContext) builder(Future stream) { return (context) { - value = useFuture(stream, initialData: null, preserveState: false); + value = useFuture(stream, preserveState: false); return Container(); }; } @@ -80,8 +80,10 @@ void main() { expect(value.data, 42); }); - Widget Function(BuildContext) snapshotText(Future stream, - {String? initialData}) { + Widget Function(BuildContext) snapshotText( + Future stream, { + String? initialData, + }) { return (context) { final snapshot = useFuture(stream, initialData: initialData); return Text(snapshot.toString(), textDirection: TextDirection.ltr); @@ -91,91 +93,136 @@ void main() { testWidgets('gracefully handles transition to other future', (tester) async { final completerA = Completer(); final completerB = Completer(); - await tester - .pumpWidget(HookBuilder(builder: snapshotText(completerA.future))); + + await tester.pumpWidget( + HookBuilder(builder: snapshotText(completerA.future)), + ); + expect( - find.text( - 'AsyncSnapshot(ConnectionState.waiting, null, null, null)'), - findsOneWidget); - await tester - .pumpWidget(HookBuilder(builder: snapshotText(completerB.future))); + find.text( + 'AsyncSnapshot(ConnectionState.waiting, null, null, null)', + ), + findsOneWidget, + ); + + await tester.pumpWidget( + HookBuilder(builder: snapshotText(completerB.future)), + ); + expect( - find.text( - 'AsyncSnapshot(ConnectionState.waiting, null, null, null)'), - findsOneWidget); + find.text( + 'AsyncSnapshot(ConnectionState.waiting, null, null, null)', + ), + findsOneWidget, + ); + completerB.complete('B'); completerA.complete('A'); await eventFiring(tester); + expect( - find.text( - 'AsyncSnapshot(ConnectionState.done, B, null, null)'), - findsOneWidget); + find.text('AsyncSnapshot(ConnectionState.done, B, null, null)'), + findsOneWidget, + ); }); + testWidgets('tracks life-cycle of Future to success', (tester) async { final completer = Completer(); - await tester - .pumpWidget(HookBuilder(builder: snapshotText(completer.future))); + await tester.pumpWidget( + HookBuilder(builder: snapshotText(completer.future)), + ); + expect( - find.text( - 'AsyncSnapshot(ConnectionState.waiting, null, null, null)'), - findsOneWidget); + find.text( + 'AsyncSnapshot(ConnectionState.waiting, null, null, null)', + ), + findsOneWidget, + ); + completer.complete('hello'); await eventFiring(tester); + expect( - find.text( - 'AsyncSnapshot(ConnectionState.done, hello, null, null)'), - findsOneWidget); + find.text( + 'AsyncSnapshot(ConnectionState.done, hello, null, null)', + ), + findsOneWidget, + ); }); + testWidgets('tracks life-cycle of Future to error', (tester) async { final completer = Completer(); - await tester - .pumpWidget(HookBuilder(builder: snapshotText(completer.future))); + await tester.pumpWidget( + HookBuilder(builder: snapshotText(completer.future)), + ); + expect( - find.text( - 'AsyncSnapshot(ConnectionState.waiting, null, null, null)'), - findsOneWidget); + find.text( + 'AsyncSnapshot(ConnectionState.waiting, null, null, null)', + ), + findsOneWidget, + ); + completer.completeError('bad', StackTrace.fromString('stackTrace')); await eventFiring(tester); + expect( - find.text( - 'AsyncSnapshot(ConnectionState.done, null, bad, stackTrace)', - ), - findsOneWidget); + find.text( + 'AsyncSnapshot(ConnectionState.done, null, bad, stackTrace)', + ), + findsOneWidget, + ); }); testWidgets('runs the builder using given initial data', (tester) async { - await tester.pumpWidget(HookBuilder( - builder: snapshotText( - Future.value(), - initialData: 'I', + await tester.pumpWidget( + HookBuilder( + builder: snapshotText( + Future.value(), + initialData: 'I', + ), ), - )); + ); + expect( - find.text( - 'AsyncSnapshot(ConnectionState.waiting, I, null, null)'), - findsOneWidget); + find.text( + 'AsyncSnapshot(ConnectionState.waiting, I, null, null)', + ), + findsOneWidget, + ); }); testWidgets('ignores initialData when reconfiguring', (tester) async { - await tester.pumpWidget(HookBuilder( - builder: snapshotText( - Future.value(), - initialData: 'I', + await tester.pumpWidget( + HookBuilder( + builder: snapshotText( + Future.value(), + initialData: 'I', + ), ), - )); + ); expect( - find.text( - 'AsyncSnapshot(ConnectionState.waiting, I, null, null)'), - findsOneWidget); + find.text( + 'AsyncSnapshot(ConnectionState.waiting, I, null, null)', + ), + findsOneWidget, + ); + final completer = Completer(); - await tester.pumpWidget(HookBuilder( - builder: snapshotText( - completer.future, - initialData: 'Ignored', + + await tester.pumpWidget( + HookBuilder( + builder: snapshotText( + completer.future, + initialData: 'Ignored', + ), ), - )); + ); + expect( - find.text( - 'AsyncSnapshot(ConnectionState.waiting, null, null, null)'), - findsOneWidget); + find.text( + 'AsyncSnapshot(ConnectionState.waiting, null, null, null)', + ), + findsOneWidget, + ); }); } diff --git a/packages/flutter_hooks/test/use_stream_test.dart b/packages/flutter_hooks/test/use_stream_test.dart index 9e31b274..8969a791 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(ConnectionState.done, 42, null,\n' + ' │ useStream: AsyncSnapshot(ConnectionState.done, 42, null,\n' ' │ null)\n' ' └SizedBox(renderObject: RenderConstrainedBox#00000)\n', ), @@ -43,7 +43,7 @@ void main() { late AsyncSnapshot? value; Widget Function(BuildContext) builder(Stream stream) { return (context) { - value = useStream(stream, initialData: null); + value = useStream(stream); return Container(); }; } @@ -65,7 +65,7 @@ void main() { late AsyncSnapshot? value; Widget Function(BuildContext) builder(Stream stream) { return (context) { - value = useStream(stream, initialData: null, preserveState: false); + value = useStream(stream, preserveState: false); return Container(); }; } @@ -98,7 +98,8 @@ void main() { .pumpWidget(HookBuilder(builder: snapshotText(controllerA.stream))); expect( find.text( - 'AsyncSnapshot(ConnectionState.waiting, , null, null)'), + 'AsyncSnapshot(ConnectionState.waiting, , null, null)', + ), findsOneWidget); await tester .pumpWidget(HookBuilder(builder: snapshotText(controllerB.stream))); @@ -107,7 +108,7 @@ void main() { await eventFiring(tester); expect( find.text( - 'AsyncSnapshot(ConnectionState.active, B, null, null)'), + 'AsyncSnapshot(ConnectionState.active, B, null, null)'), findsOneWidget); }); testWidgets('tracks events and errors of stream until completion', @@ -117,56 +118,77 @@ void main() { .pumpWidget(HookBuilder(builder: snapshotText(controller.stream))); expect( find.text( - 'AsyncSnapshot(ConnectionState.waiting, , null, null)'), + 'AsyncSnapshot(ConnectionState.waiting, , null, null)', + ), findsOneWidget); controller..add('1')..add('2'); await eventFiring(tester); expect( find.text( - 'AsyncSnapshot(ConnectionState.active, 2, null, null)'), + 'AsyncSnapshot(ConnectionState.active, 2, null, null)', + ), findsOneWidget); controller ..add('3') ..addError('bad', StackTrace.fromString('stackTrace')); + await eventFiring(tester); + expect( - find.text( - 'AsyncSnapshot(ConnectionState.active, null, bad, stackTrace)', - ), - findsOneWidget); + find.text( + 'AsyncSnapshot(ConnectionState.active, null, bad, stackTrace)', + ), + findsOneWidget, + ); + controller.add('4'); await controller.close(); await eventFiring(tester); + expect( - find.text('AsyncSnapshot(ConnectionState.done, 4, null, null)'), - findsOneWidget); + find.text('AsyncSnapshot(ConnectionState.done, 4, null, null)'), + findsOneWidget, + ); }); testWidgets('runs the builder using given initial data', (tester) async { final controller = StreamController(); - await tester.pumpWidget(HookBuilder( - builder: snapshotText(controller.stream, initialData: 'I'), - )); + await tester.pumpWidget( + HookBuilder( + builder: snapshotText(controller.stream, initialData: 'I'), + ), + ); + expect( - find.text( - 'AsyncSnapshot(ConnectionState.waiting, I, null, null)'), - findsOneWidget); + find.text( + 'AsyncSnapshot(ConnectionState.waiting, I, null, null)'), + findsOneWidget, + ); }); testWidgets('ignores initialData when reconfiguring', (tester) async { - await tester.pumpWidget(HookBuilder( - builder: snapshotText(const Stream.empty(), initialData: 'I'), - )); + await tester.pumpWidget( + HookBuilder( + builder: snapshotText(const Stream.empty(), initialData: 'I'), + ), + ); + expect( - find.text( - 'AsyncSnapshot(ConnectionState.waiting, I, null, null)'), - findsOneWidget); + find.text( + 'AsyncSnapshot(ConnectionState.waiting, I, null, null)'), + findsOneWidget, + ); + final controller = StreamController(); + await tester.pumpWidget(HookBuilder( builder: snapshotText(controller.stream, initialData: 'Ignored'), )); + expect( - find.text( - 'AsyncSnapshot(ConnectionState.waiting, I, null, null)'), - findsOneWidget); + find.text( + 'AsyncSnapshot(ConnectionState.waiting, I, null, null)', + ), + findsOneWidget, + ); }); } From 7177981e82146fa87df45058c8c6f806c7180a42 Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Thu, 13 May 2021 10:00:12 +0100 Subject: [PATCH 230/384] Update changelog --- packages/flutter_hooks/CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/flutter_hooks/CHANGELOG.md b/packages/flutter_hooks/CHANGELOG.md index 890cca2a..146a2089 100644 --- a/packages/flutter_hooks/CHANGELOG.md +++ b/packages/flutter_hooks/CHANGELOG.md @@ -2,6 +2,7 @@ - `useFuture`/`useStream`'s `AsynsSnapshot` now correctly expose the StackTrace when there is an error. - added `useRef` and `useCallback`, similar to the React equivalents. +- `initialData` of `useStream` and `useFuture` is now optional. ## 0.16.0 From 32139b9c7cb28f4ecd5cd0d9016bb99bbbdf6bd4 Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Thu, 13 May 2021 10:00:34 +0100 Subject: [PATCH 231/384] fix test --- packages/flutter_hooks/test/use_stream_test.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/flutter_hooks/test/use_stream_test.dart b/packages/flutter_hooks/test/use_stream_test.dart index 8969a791..f5ae7c80 100644 --- a/packages/flutter_hooks/test/use_stream_test.dart +++ b/packages/flutter_hooks/test/use_stream_test.dart @@ -185,7 +185,7 @@ void main() { expect( find.text( - 'AsyncSnapshot(ConnectionState.waiting, I, null, null)', + 'AsyncSnapshot(ConnectionState.waiting, I, null, null)', ), findsOneWidget, ); From aa193cb9851bdfa2401785662fd318edbe5fc76b Mon Sep 17 00:00:00 2001 From: Tanase Hagi Date: Wed, 30 Jun 2021 14:04:09 +0300 Subject: [PATCH 232/384] Add initialValue to useRef (#244) --- .../flutter_hooks/lib/src/primitives.dart | 13 ++++-- .../flutter_hooks/test/memoized_test.dart | 40 ++++++++++++++++--- 2 files changed, 43 insertions(+), 10 deletions(-) diff --git a/packages/flutter_hooks/lib/src/primitives.dart b/packages/flutter_hooks/lib/src/primitives.dart index 699a08bd..fb66146f 100644 --- a/packages/flutter_hooks/lib/src/primitives.dart +++ b/packages/flutter_hooks/lib/src/primitives.dart @@ -4,10 +4,15 @@ part of 'hooks.dart'; /// /// It is typically created by [useRef]. class ObjectRef { - /// A mutable property that will be preserved accross rebuilds. + /// A class that stores a single value; + /// + /// It is typically created by [useRef]. + ObjectRef(this.value); + + /// A mutable property that will be preserved across rebuilds. /// /// Updating this property will not cause widgets to rebuild. - T? value; + T value; } /// Creates an object that contains a single mutable property. @@ -15,8 +20,8 @@ class ObjectRef { /// Mutating the object's property has no effect. /// This is useful for sharing state accross `build` calls, without causing /// unnecessary rebuilds. -ObjectRef useRef() { - return useMemoized(() => ObjectRef()); +ObjectRef useRef(T initialValue) { + return useMemoized(() => ObjectRef(initialValue)); } /// Cache a function accross rebuilds based on a list of keys. diff --git a/packages/flutter_hooks/test/memoized_test.dart b/packages/flutter_hooks/test/memoized_test.dart index 906c46c0..da5eda49 100644 --- a/packages/flutter_hooks/test/memoized_test.dart +++ b/packages/flutter_hooks/test/memoized_test.dart @@ -11,30 +11,58 @@ void main() { reset(valueBuilder); }); - testWidgets('useRef', (tester) async { + testWidgets('useRef with null initial value', (tester) async { late ObjectRef ref; await tester.pumpWidget( HookBuilder(builder: (context) { - ref = useRef(); + ref = useRef(null); return Container(); }), ); - expect(ref.value, null); + expect(ref.value, null, reason: 'The ref value has the initial set value.'); ref.value = 42; late ObjectRef ref2; await tester.pumpWidget( HookBuilder(builder: (context) { - ref2 = useRef(); + ref2 = useRef(null); return Container(); }), ); - expect(ref2, ref); - expect(ref2.value, 42); + expect(ref2, ref, reason: 'The ref value remains the same after rebuild.'); + expect(ref2.value, 42, + reason: 'The ref value has the last assigned value.'); + }); + + testWidgets('useRef with non-null initial value', (tester) async { + late ObjectRef ref; + + await tester.pumpWidget( + HookBuilder(builder: (context) { + ref = useRef(41); + return Container(); + }), + ); + + expect(ref.value, 41, reason: 'The ref value has the initial set value.'); + ref.value = 42; + + late ObjectRef ref2; + + await tester.pumpWidget( + HookBuilder(builder: (context) { + ref2 = useRef(43); + return Container(); + }), + ); + + expect(ref2, ref, reason: 'The ref value remains the same after rebuild.'); + expect(ref2.value, 42, + reason: 'The ref value has the last assigned value.'); }); testWidgets('useCallback', (tester) async { From 67ff7db68a02c22011688cbbc81172bfb42d6b14 Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Wed, 30 Jun 2021 12:06:01 +0100 Subject: [PATCH 233/384] Update changelog --- packages/flutter_hooks/CHANGELOG.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/packages/flutter_hooks/CHANGELOG.md b/packages/flutter_hooks/CHANGELOG.md index 146a2089..707dd332 100644 --- a/packages/flutter_hooks/CHANGELOG.md +++ b/packages/flutter_hooks/CHANGELOG.md @@ -1,3 +1,17 @@ +## Unreleased major + +- **Breaking**: `useRef` now receive an initial value as parameter. + To migrate, you can change: + ```dart + ObjectRef ref = useRef(); + ``` + + to: + + ```dart + ObjectRef ref = useRef(null); + ``` + ## 0.17.0 - `useFuture`/`useStream`'s `AsynsSnapshot` now correctly expose the StackTrace when there is an error. From c1ef63475a33650dd5c7e264e2d4ce4d5455261f Mon Sep 17 00:00:00 2001 From: David Martos Date: Wed, 30 Jun 2021 13:12:11 +0200 Subject: [PATCH 234/384] Fix async hooks nullability #229 (#250) --- packages/flutter_hooks/lib/src/async.dart | 51 ++++++++++--------- .../flutter_hooks/test/use_future_test.dart | 14 ++--- .../flutter_hooks/test/use_stream_test.dart | 32 ++++++------ 3 files changed, 50 insertions(+), 47 deletions(-) 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 useFuture( +AsyncSnapshot useFuture( Future? future, { T? initialData, bool preserveState = true, @@ -22,7 +22,7 @@ AsyncSnapshot useFuture( ); } -class _FutureHook extends Hook> { +class _FutureHook extends Hook> { const _FutureHook( this.future, { required this.initialData, @@ -37,13 +37,16 @@ class _FutureHook extends Hook> { _FutureStateHook createState() => _FutureStateHook(); } -class _FutureStateHook extends HookState, _FutureHook> { +class _FutureStateHook extends HookState, _FutureHook> { /// 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 _snapshot = - AsyncSnapshot.withData(ConnectionState.none, hook.initialData); + late AsyncSnapshot _snapshot = initial; + + AsyncSnapshot get initial => hook.initialData == null + ? AsyncSnapshot.nothing() + : AsyncSnapshot.withData(ConnectionState.none, hook.initialData as T); @override void initHook() { @@ -60,8 +63,7 @@ class _FutureStateHook extends HookState, _FutureHook> { if (hook.preserveState) { _snapshot = _snapshot.inState(ConnectionState.none); } else { - _snapshot = AsyncSnapshot.withData( - ConnectionState.none, hook.initialData); + _snapshot = initial; } } _subscribe(); @@ -80,14 +82,14 @@ class _FutureStateHook extends HookState, _FutureHook> { hook.future!.then((data) { if (_activeCallbackIdentity == callbackIdentity) { setState(() { - _snapshot = AsyncSnapshot.withData(ConnectionState.done, data); + _snapshot = AsyncSnapshot.withData(ConnectionState.done, data); }); } // ignore: avoid_types_on_closure_parameters }, onError: (Object error, StackTrace stackTrace) { if (_activeCallbackIdentity == callbackIdentity) { setState(() { - _snapshot = AsyncSnapshot.withError( + _snapshot = AsyncSnapshot.withError( ConnectionState.done, error, stackTrace, @@ -104,7 +106,7 @@ class _FutureStateHook extends HookState, _FutureHook> { } @override - AsyncSnapshot build(BuildContext context) { + AsyncSnapshot build(BuildContext context) { return _snapshot; } @@ -123,7 +125,7 @@ class _FutureStateHook extends HookState, _FutureHook> { /// See also: /// * [Stream], the object listened. /// * [useFuture], similar to [useStream] but for [Future]. -AsyncSnapshot useStream( +AsyncSnapshot useStream( Stream? stream, { T? initialData, bool preserveState = true, @@ -137,7 +139,7 @@ AsyncSnapshot useStream( ); } -class _StreamHook extends Hook> { +class _StreamHook extends Hook> { const _StreamHook( this.stream, { required this.initialData, @@ -153,9 +155,9 @@ class _StreamHook extends Hook> { } /// a clone of [StreamBuilderBase] implementation -class _StreamHookState extends HookState, _StreamHook> { +class _StreamHookState extends HookState, _StreamHook> { StreamSubscription? _subscription; - late AsyncSnapshot _summary = initial; + late AsyncSnapshot _summary = initial; @override void initHook() { @@ -210,32 +212,33 @@ class _StreamHookState extends HookState, _StreamHook> { } @override - AsyncSnapshot build(BuildContext context) { + AsyncSnapshot build(BuildContext context) { return _summary; } - AsyncSnapshot get initial => - AsyncSnapshot.withData(ConnectionState.none, hook.initialData); + AsyncSnapshot get initial => hook.initialData == null + ? AsyncSnapshot.nothing() + : AsyncSnapshot.withData(ConnectionState.none, hook.initialData as T); - AsyncSnapshot afterConnected(AsyncSnapshot current) => + AsyncSnapshot afterConnected(AsyncSnapshot current) => current.inState(ConnectionState.waiting); - AsyncSnapshot afterData(T data) { - return AsyncSnapshot.withData(ConnectionState.active, data); + AsyncSnapshot afterData(T data) { + return AsyncSnapshot.withData(ConnectionState.active, data); } - AsyncSnapshot afterError(Object error, StackTrace stackTrace) { - return AsyncSnapshot.withError( + AsyncSnapshot afterError(Object error, StackTrace stackTrace) { + return AsyncSnapshot.withError( ConnectionState.active, error, stackTrace, ); } - AsyncSnapshot afterDone(AsyncSnapshot current) => + AsyncSnapshot afterDone(AsyncSnapshot current) => current.inState(ConnectionState.done); - AsyncSnapshot afterDisconnected(AsyncSnapshot current) => + AsyncSnapshot afterDisconnected(AsyncSnapshot current) => current.inState(ConnectionState.none); @override 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 value; - Widget Function(BuildContext) builder(Future stream) { + late AsyncSnapshot value; + Widget Function(BuildContext) builder(Future stream) { return (context) { - value = useFuture(stream); + value = useFuture(stream); return Container(); }; } @@ -50,7 +50,7 @@ void main() { .toStringDeep(), equalsIgnoringHashCodes( 'HookBuilder\n' - ' │ useFuture: AsyncSnapshot(ConnectionState.done, 42, null,\n' + ' │ useFuture: AsyncSnapshot(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 value; - Widget Function(BuildContext) builder(Future stream) { + late AsyncSnapshot value; + Widget Function(BuildContext) builder(Future stream) { return (context) { - value = useFuture(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(ConnectionState.done, 42, null,\n' + ' │ useStream: AsyncSnapshot(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? value; - Widget Function(BuildContext) builder(Stream stream) { + late AsyncSnapshot? value; + Widget Function(BuildContext) builder(Stream stream) { return (context) { - value = useStream(stream); + value = useStream(stream); return Container(); }; } @@ -62,10 +62,10 @@ void main() { }); testWidgets('If preserveState == false, changing stream resets value', (tester) async { - late AsyncSnapshot? value; - Widget Function(BuildContext) builder(Stream stream) { + late AsyncSnapshot? value; + Widget Function(BuildContext) builder(Stream stream) { return (context) { - value = useStream(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(ConnectionState.waiting, , null, null)', + 'AsyncSnapshot(ConnectionState.waiting, , null, null)', ), findsOneWidget); await tester @@ -108,7 +108,7 @@ void main() { await eventFiring(tester); expect( find.text( - 'AsyncSnapshot(ConnectionState.active, B, null, null)'), + 'AsyncSnapshot(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(ConnectionState.waiting, , null, null)', + 'AsyncSnapshot(ConnectionState.waiting, , null, null)', ), findsOneWidget); controller..add('1')..add('2'); await eventFiring(tester); expect( find.text( - 'AsyncSnapshot(ConnectionState.active, 2, null, null)', + 'AsyncSnapshot(ConnectionState.active, 2, null, null)', ), findsOneWidget); controller @@ -136,7 +136,7 @@ void main() { expect( find.text( - 'AsyncSnapshot(ConnectionState.active, null, bad, stackTrace)', + 'AsyncSnapshot(ConnectionState.active, null, bad, stackTrace)', ), findsOneWidget, ); @@ -146,7 +146,7 @@ void main() { await eventFiring(tester); expect( - find.text('AsyncSnapshot(ConnectionState.done, 4, null, null)'), + find.text('AsyncSnapshot(ConnectionState.done, 4, null, null)'), findsOneWidget, ); }); @@ -160,7 +160,7 @@ void main() { expect( find.text( - 'AsyncSnapshot(ConnectionState.waiting, I, null, null)'), + 'AsyncSnapshot(ConnectionState.waiting, I, null, null)'), findsOneWidget, ); }); @@ -173,7 +173,7 @@ void main() { expect( find.text( - 'AsyncSnapshot(ConnectionState.waiting, I, null, null)'), + 'AsyncSnapshot(ConnectionState.waiting, I, null, null)'), findsOneWidget, ); @@ -185,7 +185,7 @@ void main() { expect( find.text( - 'AsyncSnapshot(ConnectionState.waiting, I, null, null)', + 'AsyncSnapshot(ConnectionState.waiting, I, null, null)', ), findsOneWidget, ); From 11a0310466629601555914ee7c3c3cba16850ab7 Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Wed, 30 Jun 2021 12:13:07 +0100 Subject: [PATCH 235/384] Update changelog --- packages/flutter_hooks/CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/flutter_hooks/CHANGELOG.md b/packages/flutter_hooks/CHANGELOG.md index 707dd332..8030f69a 100644 --- a/packages/flutter_hooks/CHANGELOG.md +++ b/packages/flutter_hooks/CHANGELOG.md @@ -12,6 +12,8 @@ ObjectRef ref = useRef(null); ``` +- Updated `useStream`/`useFuture` to match the behavior of `StreamBuilder`/`FutureBuilder` regarding initial values. + ## 0.17.0 - `useFuture`/`useStream`'s `AsynsSnapshot` now correctly expose the StackTrace when there is an error. From 2990f10017f79ff96d126e812bf06f0a3a4e3be9 Mon Sep 17 00:00:00 2001 From: munitum <42879497+munitum@users.noreply.github.com> Date: Wed, 30 Jun 2021 04:20:27 -0700 Subject: [PATCH 236/384] Use reverseDuration in useAnimationController (#248) Co-authored-by: Max Kant --- packages/flutter_hooks/lib/src/animation.dart | 10 ++++++++++ .../test/use_animation_controller_test.dart | 10 +++++++++- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/packages/flutter_hooks/lib/src/animation.dart b/packages/flutter_hooks/lib/src/animation.dart index dcb50718..32ad92d1 100644 --- a/packages/flutter_hooks/lib/src/animation.dart +++ b/packages/flutter_hooks/lib/src/animation.dart @@ -42,6 +42,7 @@ class _UseAnimationStateHook extends _ListenableStateHook { /// * [useAnimation], to listen to the created [AnimationController]. AnimationController useAnimationController({ Duration? duration, + Duration? reverseDuration, String? debugLabel, double initialValue = 0, double lowerBound = 0, @@ -55,6 +56,7 @@ AnimationController useAnimationController({ return use( _AnimationControllerHook( duration: duration, + reverseDuration: reverseDuration, debugLabel: debugLabel, initialValue: initialValue, lowerBound: lowerBound, @@ -69,6 +71,7 @@ AnimationController useAnimationController({ class _AnimationControllerHook extends Hook { const _AnimationControllerHook({ this.duration, + this.reverseDuration, this.debugLabel, required this.initialValue, required this.lowerBound, @@ -79,6 +82,7 @@ class _AnimationControllerHook extends Hook { }) : super(keys: keys); final Duration? duration; + final Duration? reverseDuration; final String? debugLabel; final double initialValue; final double lowerBound; @@ -94,6 +98,7 @@ class _AnimationControllerHook extends Hook { void debugFillProperties(DiagnosticPropertiesBuilder properties) { super.debugFillProperties(properties); properties.add(DiagnosticsProperty('duration', duration)); + properties.add(DiagnosticsProperty('reverseDuration', reverseDuration)); } } @@ -102,6 +107,7 @@ class _AnimationControllerHookState late final AnimationController _animationController = AnimationController( vsync: hook.vsync, duration: hook.duration, + reverseDuration: hook.reverseDuration, debugLabel: hook.debugLabel, lowerBound: hook.lowerBound, upperBound: hook.upperBound, @@ -119,6 +125,10 @@ class _AnimationControllerHookState if (hook.duration != oldHook.duration) { _animationController.duration = hook.duration; } + + if (hook.reverseDuration != oldHook.reverseDuration) { + _animationController.reverseDuration = hook.reverseDuration; + } } @override diff --git a/packages/flutter_hooks/test/use_animation_controller_test.dart b/packages/flutter_hooks/test/use_animation_controller_test.dart index d424a941..bf766aae 100644 --- a/packages/flutter_hooks/test/use_animation_controller_test.dart +++ b/packages/flutter_hooks/test/use_animation_controller_test.dart @@ -17,6 +17,7 @@ void main() { ); expect(controller.duration, isNull); + expect(controller.reverseDuration, isNull); expect(controller.lowerBound, 0); expect(controller.upperBound, 1); expect(controller.value, 0); @@ -25,6 +26,7 @@ void main() { controller ..duration = const Duration(seconds: 1) + ..reverseDuration = const Duration(seconds: 1) // check has a ticker ..forward(); @@ -38,6 +40,7 @@ void main() { useAnimationController( animationBehavior: AnimationBehavior.preserve, duration: const Duration(seconds: 1), + reverseDuration: const Duration(milliseconds: 500), initialValue: 42, lowerBound: 24, upperBound: 84, @@ -58,7 +61,8 @@ void main() { ' │ useSingleTickerProvider\n' ' │ useAnimationController:\n' ' │ _AnimationControllerHookState#00000(AnimationController#00000(▶\n' - ' │ 42.000; paused; for Foo), duration: 0:00:01.000000)\n' + ' │ 42.000; paused; for Foo), duration: 0:00:01.000000,\n' + ' │ reverseDuration: 0:00:00.500000)\n' ' └SizedBox(renderObject: RenderConstrainedBox#00000)\n', ), ); @@ -80,6 +84,7 @@ void main() { vsync: provider, animationBehavior: AnimationBehavior.preserve, duration: const Duration(seconds: 1), + reverseDuration: const Duration(milliseconds: 500), initialValue: 42, lowerBound: 24, upperBound: 84, @@ -96,6 +101,7 @@ void main() { // ignore: unawaited_futures controller.forward(); expect(controller.duration, const Duration(seconds: 1)); + expect(controller.reverseDuration, const Duration(milliseconds: 500)); expect(controller.lowerBound, 24); expect(controller.upperBound, 84); expect(controller.value, 42); @@ -113,6 +119,7 @@ void main() { controller = useAnimationController( vsync: provider, duration: const Duration(seconds: 2), + reverseDuration: const Duration(seconds: 1), debugLabel: 'Bar', ); return Container(); @@ -123,6 +130,7 @@ void main() { verifyNoMoreInteractions(provider); expect(controller, previousController); expect(controller.duration, const Duration(seconds: 2)); + expect(controller.reverseDuration, const Duration(seconds: 1)); expect(controller.lowerBound, 24); expect(controller.upperBound, 84); expect(controller.value, 42); From 7c7459d735b589c17c74dd88cb2c85b72036dd48 Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Wed, 30 Jun 2021 12:21:43 +0100 Subject: [PATCH 237/384] 0.18.0 --- packages/flutter_hooks/CHANGELOG.md | 4 +++- packages/flutter_hooks/pubspec.yaml | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/flutter_hooks/CHANGELOG.md b/packages/flutter_hooks/CHANGELOG.md index 8030f69a..19cacff8 100644 --- a/packages/flutter_hooks/CHANGELOG.md +++ b/packages/flutter_hooks/CHANGELOG.md @@ -1,4 +1,4 @@ -## Unreleased major +## 0.18.0 - **Breaking**: `useRef` now receive an initial value as parameter. To migrate, you can change: @@ -14,6 +14,8 @@ - Updated `useStream`/`useFuture` to match the behavior of `StreamBuilder`/`FutureBuilder` regarding initial values. +- Added a `reverseDuration` parameter to `useAnimationController` + ## 0.17.0 - `useFuture`/`useStream`'s `AsynsSnapshot` now correctly expose the StackTrace when there is an error. diff --git a/packages/flutter_hooks/pubspec.yaml b/packages/flutter_hooks/pubspec.yaml index d7e7ab85..dacdfc90 100644 --- a/packages/flutter_hooks/pubspec.yaml +++ b/packages/flutter_hooks/pubspec.yaml @@ -1,7 +1,7 @@ name: flutter_hooks description: A flutter implementation of React hooks. It adds a new kind of widget with enhanced code reuse. homepage: https://github.com/rrousselGit/flutter_hooks -version: 0.17.0 +version: 0.18.0 environment: sdk: ">=2.12.0-0 <3.0.0" From 46c278805a0af2135f98e6bab1a2625e10da1e59 Mon Sep 17 00:00:00 2001 From: Mohammad Sadegh Shad Date: Tue, 17 Aug 2021 12:04:50 +0430 Subject: [PATCH 238/384] fix: Fixed a typo (#260) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 123fe715..c5e674e2 100644 --- a/README.md +++ b/README.md @@ -300,7 +300,7 @@ A set of low-level hooks that interacts with the different life-cycles of a widg | [useState](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useState.html) | Create variable and subscribes to it. | | [useMemoized](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useMemoized.html) | Cache the instance of a complex object. | | [useRef](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useRef.html) | Creates an object that contains a single mutable property. | -| [useCallback](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useCallback.html) | Cache a function insteance. | +| [useCallback](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useCallback.html) | Cache a function instance. | | [useContext](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useContext.html) | Obtain the `BuildContext` of the building `HookWidget`. | | [useValueChanged](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useValueChanged.html) | Watches a value and calls a callback whenever the value changed. | From 1d94f63afe4d496d4f4b08bfd8789cadfef7f0e5 Mon Sep 17 00:00:00 2001 From: Teppei Imai Date: Sat, 18 Sep 2021 20:31:22 +0900 Subject: [PATCH 239/384] fixed root README typo (#264) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index c5e674e2..2029c8cb 100644 --- a/README.md +++ b/README.md @@ -359,7 +359,7 @@ For a custom-hook to be merged, you will need to do the following: - Describe the use-case. Open an issue explaining why we need this hook, how to use it, ... - This is important as a hook will not get merged if the hook doens't appeal to + This is important as a hook will not get merged if the hook doesn't appeal to a large number of people. If your hook is rejected, don't worry! A rejection doesn't mean that it won't From ee1ea81d95ad6028605d9aa8411ed1f06342e8eb Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Sun, 19 Sep 2021 21:09:34 +0100 Subject: [PATCH 240/384] Add discord badge --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 2029c8cb..3a913aae 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@ [English](https://github.com/rrousselGit/flutter_hooks/blob/master/README.md) | [Português](https://github.com/rrousselGit/flutter_hooks/blob/master/packages/flutter_hooks/resources/translations/pt_br/README.md) [![Build](https://github.com/rrousselGit/flutter_hooks/workflows/Build/badge.svg)](https://github.com/rrousselGit/flutter_hooks/actions?query=workflow%3ABuild) [![codecov](https://codecov.io/gh/rrousselGit/flutter_hooks/branch/master/graph/badge.svg)](https://codecov.io/gh/rrousselGit/flutter_hooks) [![pub package](https://img.shields.io/pub/v/flutter_hooks.svg)](https://pub.dartlang.org/packages/flutter_hooks) [![pub package](https://img.shields.io/badge/Awesome-Flutter-blue.svg?longCache=true&style=flat-square)](https://github.com/Solido/awesome-flutter) +Discord From 7795fccaeed59eee8e0e1c3101afad08d41c6e71 Mon Sep 17 00:00:00 2001 From: "roux (none/they)" <3411715+shrouxm@users.noreply.github.com> Date: Wed, 10 Nov 2021 04:34:01 -0800 Subject: [PATCH 241/384] add a useTransformationController hook (#254) --- README.md | 1 + packages/flutter_hooks/lib/src/hooks.dart | 1 + .../lib/src/transformation_controller.dart | 44 +++++++++ .../resources/translations/pt_br/README.md | 1 + .../test/use_transformation_controller.dart | 93 +++++++++++++++++++ 5 files changed, 140 insertions(+) create mode 100644 packages/flutter_hooks/lib/src/transformation_controller.dart create mode 100644 packages/flutter_hooks/test/use_transformation_controller.dart diff --git a/README.md b/README.md index 3a913aae..b9db6f3e 100644 --- a/README.md +++ b/README.md @@ -346,6 +346,7 @@ A series of hooks with no particular theme. | [useFocusNode](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useFocusNode.html) | Create a `FocusNode` | | [useTabController](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useTabController.html) | Creates and disposes a `TabController`. | | [useScrollController](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useScrollController.html) | Creates and disposes a `ScrollController`. | +| [useTransformationController](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useTransformationController.html) | Creates and disposes a `TransformationController`. | | [usePageController](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/usePageController.html) | Creates and disposes a `PageController`. | | [useIsMounted](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useIsMounted.html) | An equivalent to `State.mounted` for hooks | diff --git a/packages/flutter_hooks/lib/src/hooks.dart b/packages/flutter_hooks/lib/src/hooks.dart index 0cade07f..d379f969 100644 --- a/packages/flutter_hooks/lib/src/hooks.dart +++ b/packages/flutter_hooks/lib/src/hooks.dart @@ -17,3 +17,4 @@ part 'text_controller.dart'; part 'focus.dart'; part 'scroll_controller.dart'; part 'page_controller.dart'; +part 'transformation_controller.dart'; diff --git a/packages/flutter_hooks/lib/src/transformation_controller.dart b/packages/flutter_hooks/lib/src/transformation_controller.dart new file mode 100644 index 00000000..25d67d78 --- /dev/null +++ b/packages/flutter_hooks/lib/src/transformation_controller.dart @@ -0,0 +1,44 @@ +part of 'hooks.dart'; + +/// Creates and disposes a [TransformationController]. +/// +/// See also: +/// - [TransformationController] +TransformationController useTransformationController({ + Matrix4? initialValue, + List? keys, +}) { + return use( + _TransformationControllerHook( + initialValue: initialValue, + keys: keys, + ), + ); +} + +class _TransformationControllerHook extends Hook { + const _TransformationControllerHook({ + required this.initialValue, + List? keys, + }) : super(keys: keys); + + final Matrix4? initialValue; + + @override + HookState> createState() => + _TransformationControllerHookState(); +} + +class _TransformationControllerHookState + extends HookState { + late final controller = TransformationController(hook.initialValue); + + @override + TransformationController build(BuildContext context) => controller; + + @override + void dispose() => controller.dispose(); + + @override + String get debugLabel => 'useTransformationController'; +} diff --git a/packages/flutter_hooks/resources/translations/pt_br/README.md b/packages/flutter_hooks/resources/translations/pt_br/README.md index 4d615e38..556dd62b 100644 --- a/packages/flutter_hooks/resources/translations/pt_br/README.md +++ b/packages/flutter_hooks/resources/translations/pt_br/README.md @@ -349,6 +349,7 @@ São vários hooks sem um tema particular. | [useFocusNode](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useFocusNode.html) | Cria um `FocusNode` | | [useTabController](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useTabController.html) | Cria e descarta um `TabController`. | | [useScrollController](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useScrollController.html) | Cria e descarta um `ScrollController`. | +| [useTransformationController](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useTransformationController.html) | Cria e descarta um `TransformationController`. | ## Contribuições diff --git a/packages/flutter_hooks/test/use_transformation_controller.dart b/packages/flutter_hooks/test/use_transformation_controller.dart new file mode 100644 index 00000000..fee23eb6 --- /dev/null +++ b/packages/flutter_hooks/test/use_transformation_controller.dart @@ -0,0 +1,93 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/scheduler.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) { + useTransformationController(); + return const SizedBox(); + }), + ); + + final element = tester.element(find.byType(HookBuilder)); + + expect( + element + .toDiagnosticsNode(style: DiagnosticsTreeStyle.offstage) + .toStringDeep(), + equalsIgnoringHashCodes( + 'HookBuilder\n' + ' │ useTransformationController: TransformationController#00000(no clients)\n' + ' └SizedBox(renderObject: RenderConstrainedBox#00000)\n', + ), + ); + }); + + group('useTransformationController', () { + testWidgets('initial values matches with real constructor', (tester) async { + late TransformationController controller; + late TransformationController controller2; + + await tester.pumpWidget( + HookBuilder(builder: (context) { + controller2 = TransformationController(); + controller = useTransformationController(); + return Container(); + }), + ); + + expect(controller.value, controller2.value); + }); + testWidgets("returns a TransformationController that doesn't change", + (tester) async { + late TransformationController controller; + late TransformationController controller2; + + await tester.pumpWidget( + HookBuilder(builder: (context) { + controller = useTransformationController(); + return Container(); + }), + ); + + expect(controller, isA()); + + await tester.pumpWidget( + HookBuilder(builder: (context) { + controller2 = useTransformationController(); + return Container(); + }), + ); + + expect(identical(controller, controller2), isTrue); + }); + + testWidgets('passes hook parameters to the TransformationController', + (tester) async { + late TransformationController controller; + + await tester.pumpWidget( + HookBuilder( + builder: (context) { + controller = useTransformationController( + initialValue: Matrix4.translationValues(1, 2, 3), + ); + + return Container(); + }, + ), + ); + + expect(controller.value, Matrix4.translationValues(1, 2, 3)); + }); + }); +} + +class TickerProviderMock extends Mock implements TickerProvider {} From 526a958c6a6a8942c79b6480b46e5d66ebfe5616 Mon Sep 17 00:00:00 2001 From: "Francisco R. Del Roio" Date: Wed, 10 Nov 2021 09:37:02 -0300 Subject: [PATCH 242/384] Added an `useAppStateLifecycle` hook (#259) Co-authored-by: Remi Rousselet --- README.md | 26 +++---- packages/flutter_hooks/lib/src/framework.dart | 1 - packages/flutter_hooks/lib/src/hooks.dart | 1 + .../lib/src/widgets_binding_observer.dart | 65 +++++++++++++++++ packages/flutter_hooks/pubspec.yaml | 2 +- packages/flutter_hooks/test/mock.dart | 4 +- .../test/pre_build_abort_test.dart | 2 - .../test/use_app_lifecycle_state_test.dart | 70 +++++++++++++++++++ .../test/use_scroll_controller_test.dart | 2 - .../flutter_hooks/test/use_state_test.dart | 1 - .../flutter_hooks/test/use_stream_test.dart | 4 +- .../test/use_tab_controller_test.dart | 1 - .../use_text_editing_controller_test.dart | 5 +- .../test/use_ticker_provider_test.dart | 1 - 14 files changed, 159 insertions(+), 26 deletions(-) create mode 100644 packages/flutter_hooks/lib/src/widgets_binding_observer.dart create mode 100644 packages/flutter_hooks/test/use_app_lifecycle_state_test.dart diff --git a/README.md b/README.md index b9db6f3e..f3a4be4a 100644 --- a/README.md +++ b/README.md @@ -301,7 +301,7 @@ A set of low-level hooks that interacts with the different life-cycles of a widg | [useState](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useState.html) | Create variable and subscribes to it. | | [useMemoized](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useMemoized.html) | Cache the instance of a complex object. | | [useRef](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useRef.html) | Creates an object that contains a single mutable property. | -| [useCallback](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useCallback.html) | Cache a function instance. | +| [useCallback](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useCallback.html) | Cache a function instance. | | [useContext](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useContext.html) | Obtain the `BuildContext` of the building `HookWidget`. | | [useValueChanged](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useValueChanged.html) | Watches a value and calls a callback whenever the value changed. | @@ -338,17 +338,19 @@ They will take care of creating/updating/disposing an object. A series of hooks with no particular theme. -| name | description | -| ----------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------ | -| [useReducer](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useReducer.html) | An alternative to `useState` for more complex states. | -| [usePrevious](https://pub.dartlang.org/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) | Create a `TextEditingController` | -| [useFocusNode](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useFocusNode.html) | Create a `FocusNode` | -| [useTabController](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useTabController.html) | Creates and disposes a `TabController`. | -| [useScrollController](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useScrollController.html) | Creates and disposes a `ScrollController`. | -| [useTransformationController](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useTransformationController.html) | Creates and disposes a `TransformationController`. | -| [usePageController](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/usePageController.html) | Creates and disposes a `PageController`. | -| [useIsMounted](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useIsMounted.html) | An equivalent to `State.mounted` for hooks | +| name | description | +| ------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------- | +| [useReducer](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useReducer.html) | An alternative to `useState` for more complex states. | +| [usePrevious](https://pub.dartlang.org/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) | Create a `TextEditingController` | +| [useFocusNode](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useFocusNode.html) | Create a `FocusNode` | +| [useTabController](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useTabController.html) | Creates and disposes a `TabController`. | +| [useScrollController](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useScrollController.html) | Creates and disposes a `ScrollController`. | +| [usePageController](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/usePageController.html) | Creates and disposes a `PageController`. | +| [useAppLifecycleState](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useAppLifecycleState.html) | Returns the current `AppLifecycleState` and rebuild the widget on change. | +| [useOnAppLifecycleStateChange](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useOnAppLifecycleStateChange.html) | Listens to `AppLifecycleState` changes and call a callback on change. | +| [useTransformationController](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useTransformationController.html) | Creates and disposes a `TransformationController`. | +| [useIsMounted](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useIsMounted.html) | An equivalent to `State.mounted` for hooks | ## Contributions diff --git a/packages/flutter_hooks/lib/src/framework.dart b/packages/flutter_hooks/lib/src/framework.dart index 1795b2a7..97a256b2 100644 --- a/packages/flutter_hooks/lib/src/framework.dart +++ b/packages/flutter_hooks/lib/src/framework.dart @@ -1,7 +1,6 @@ import 'dart:collection'; import 'package:flutter/foundation.dart'; -import 'package:flutter/scheduler.dart'; import 'package:flutter/widgets.dart'; /// Wether to behave like in release mode or allow hot-reload for hooks. diff --git a/packages/flutter_hooks/lib/src/hooks.dart b/packages/flutter_hooks/lib/src/hooks.dart index d379f969..889a03ea 100644 --- a/packages/flutter_hooks/lib/src/hooks.dart +++ b/packages/flutter_hooks/lib/src/hooks.dart @@ -17,4 +17,5 @@ part 'text_controller.dart'; part 'focus.dart'; part 'scroll_controller.dart'; part 'page_controller.dart'; +part 'widgets_binding_observer.dart'; part 'transformation_controller.dart'; diff --git a/packages/flutter_hooks/lib/src/widgets_binding_observer.dart b/packages/flutter_hooks/lib/src/widgets_binding_observer.dart new file mode 100644 index 00000000..09ab34aa --- /dev/null +++ b/packages/flutter_hooks/lib/src/widgets_binding_observer.dart @@ -0,0 +1,65 @@ +part of 'hooks.dart'; + +/// A callback to call when the app lifecycle changes. +typedef LifecycleCallback = FutureOr Function( + AppLifecycleState? previous, + AppLifecycleState current, +); + +/// Returns the current [AppLifecycleState] value and rebuild the widget when it changes. +AppLifecycleState? useAppLifecycleState() { + return use(const _AppLifecycleHook(rebuildOnChange: true)); +} + +/// Listens to the [AppLifecycleState]. +void useOnAppLifecycleStateChange(LifecycleCallback? onStateChanged) { + use(_AppLifecycleHook(onStateChanged: onStateChanged)); +} + +class _AppLifecycleHook extends Hook { + const _AppLifecycleHook({ + this.rebuildOnChange = false, + this.onStateChanged, + }) : super(); + + final bool rebuildOnChange; + final LifecycleCallback? onStateChanged; + + @override + __AppLifecycleStateState createState() => __AppLifecycleStateState(); +} + +class __AppLifecycleStateState + extends HookState + with + // ignore: prefer_mixin + WidgetsBindingObserver { + AppLifecycleState? _state; + + @override + void initHook() { + super.initHook(); + _state = WidgetsBinding.instance!.lifecycleState; + WidgetsBinding.instance!.addObserver(this); + } + + @override + AppLifecycleState? build(BuildContext context) => _state; + + @override + void dispose() { + super.dispose(); + WidgetsBinding.instance!.removeObserver(this); + } + + @override + void didChangeAppLifecycleState(AppLifecycleState state) { + final previous = _state; + _state = state; + hook.onStateChanged?.call(previous, state); + + if (hook.rebuildOnChange) { + setState(() {}); + } + } +} diff --git a/packages/flutter_hooks/pubspec.yaml b/packages/flutter_hooks/pubspec.yaml index dacdfc90..ee9a6c1d 100644 --- a/packages/flutter_hooks/pubspec.yaml +++ b/packages/flutter_hooks/pubspec.yaml @@ -14,4 +14,4 @@ dependencies: dev_dependencies: flutter_test: sdk: flutter - mockito: "^5.0.0-nullsafety.5" + mockito: ^5.0.16 \ No newline at end of file diff --git a/packages/flutter_hooks/test/mock.dart b/packages/flutter_hooks/test/mock.dart index 69dc271e..d2fee245 100644 --- a/packages/flutter_hooks/test/mock.dart +++ b/packages/flutter_hooks/test/mock.dart @@ -97,7 +97,9 @@ Element _rootOf(Element element) { void hotReload(WidgetTester tester) { final root = _rootOf(tester.allElements.first); - TestWidgetsFlutterBinding.ensureInitialized().buildOwner?.reassemble(root); + TestWidgetsFlutterBinding.ensureInitialized() + .buildOwner + ?.reassemble(root, null); } class MockSetState extends Mock { diff --git a/packages/flutter_hooks/test/pre_build_abort_test.dart b/packages/flutter_hooks/test/pre_build_abort_test.dart index b6726365..63764fed 100644 --- a/packages/flutter_hooks/test/pre_build_abort_test.dart +++ b/packages/flutter_hooks/test/pre_build_abort_test.dart @@ -1,7 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:flutter/src/widgets/framework.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:flutter_test/flutter_test.dart'; import 'mock.dart'; diff --git a/packages/flutter_hooks/test/use_app_lifecycle_state_test.dart b/packages/flutter_hooks/test/use_app_lifecycle_state_test.dart new file mode 100644 index 00000000..17e32951 --- /dev/null +++ b/packages/flutter_hooks/test/use_app_lifecycle_state_test.dart @@ -0,0 +1,70 @@ +import 'package:flutter/widgets.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; + +import 'mock.dart'; + +void main() { + group('useAppLifecycleState', () { + testWidgets('returns initial value and rebuild widgets on change', + (tester) async { + tester.binding.handleAppLifecycleStateChanged(AppLifecycleState.resumed); + + await tester.pumpWidget( + HookBuilder( + builder: (context) { + final state = useAppLifecycleState(); + return Text('$state', textDirection: TextDirection.ltr); + }, + ), + ); + + expect(find.text('AppLifecycleState.resumed'), findsOneWidget); + + tester.binding.handleAppLifecycleStateChanged(AppLifecycleState.inactive); + await tester.pump(); + + expect(find.text('AppLifecycleState.inactive'), findsOneWidget); + }); + }); + + group('useOnAppLifecycleStateChange', () { + testWidgets( + 'sends previous and new value on change, without rebuilding widgets', + (tester) async { + tester.binding.handleAppLifecycleStateChanged(AppLifecycleState.resumed); + var buildCount = 0; + final listener = AppLifecycleStateListener(); + + await tester.pumpWidget( + HookBuilder( + builder: (context) { + buildCount++; + useOnAppLifecycleStateChange(listener); + return Container(); + }, + ), + ); + + expect(buildCount, 1); + verifyZeroInteractions(listener); + + tester.binding.handleAppLifecycleStateChanged(AppLifecycleState.paused); + await tester.pump(); + + expect(buildCount, 1); + verify(listener(AppLifecycleState.resumed, AppLifecycleState.paused)); + verifyNoMoreInteractions(listener); + + tester.binding.handleAppLifecycleStateChanged(AppLifecycleState.resumed); + await tester.pump(); + + expect(buildCount, 1); + verify(listener(AppLifecycleState.paused, AppLifecycleState.resumed)); + verifyNoMoreInteractions(listener); + }); + }); +} + +class AppLifecycleStateListener extends Mock { + void call(AppLifecycleState? prev, AppLifecycleState state); +} diff --git a/packages/flutter_hooks/test/use_scroll_controller_test.dart b/packages/flutter_hooks/test/use_scroll_controller_test.dart index 4a3a34fa..7ae60b47 100644 --- a/packages/flutter_hooks/test/use_scroll_controller_test.dart +++ b/packages/flutter_hooks/test/use_scroll_controller_test.dart @@ -1,9 +1,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/scheduler.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'; diff --git a/packages/flutter_hooks/test/use_state_test.dart b/packages/flutter_hooks/test/use_state_test.dart index 4c32073f..e38b6bac 100644 --- a/packages/flutter_hooks/test/use_state_test.dart +++ b/packages/flutter_hooks/test/use_state_test.dart @@ -1,4 +1,3 @@ -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; diff --git a/packages/flutter_hooks/test/use_stream_test.dart b/packages/flutter_hooks/test/use_stream_test.dart index f48b9146..497ae8c0 100644 --- a/packages/flutter_hooks/test/use_stream_test.dart +++ b/packages/flutter_hooks/test/use_stream_test.dart @@ -121,7 +121,9 @@ void main() { 'AsyncSnapshot(ConnectionState.waiting, , null, null)', ), findsOneWidget); - controller..add('1')..add('2'); + controller + ..add('1') + ..add('2'); await eventFiring(tester); expect( find.text( diff --git a/packages/flutter_hooks/test/use_tab_controller_test.dart b/packages/flutter_hooks/test/use_tab_controller_test.dart index b05a6348..80ddaf97 100644 --- a/packages/flutter_hooks/test/use_tab_controller_test.dart +++ b/packages/flutter_hooks/test/use_tab_controller_test.dart @@ -3,7 +3,6 @@ import 'package:flutter/material.dart'; import 'package:flutter/scheduler.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'; diff --git a/packages/flutter_hooks/test/use_text_editing_controller_test.dart b/packages/flutter_hooks/test/use_text_editing_controller_test.dart index ce119ca8..acce0f5e 100644 --- a/packages/flutter_hooks/test/use_text_editing_controller_test.dart +++ b/packages/flutter_hooks/test/use_text_editing_controller_test.dart @@ -27,9 +27,8 @@ void main() { 'HookBuilder\n' ' │ useTextEditingController:\n' ' │ TextEditingController#00000(TextEditingValue(text: ┤├,\n' - ' │ selection: TextSelection(baseOffset: -1, extentOffset: -1,\n' - ' │ affinity: TextAffinity.downstream, isDirectional: false),\n' - ' │ composing: TextRange(start: -1, end: -1)))\n' + ' │ selection: TextSelection.invalid, composing: TextRange(start:\n' + ' │ -1, end: -1)))\n' ' └SizedBox(renderObject: RenderConstrainedBox#00000)\n', ), ); diff --git a/packages/flutter_hooks/test/use_ticker_provider_test.dart b/packages/flutter_hooks/test/use_ticker_provider_test.dart index 8efdb612..329fea41 100644 --- a/packages/flutter_hooks/test/use_ticker_provider_test.dart +++ b/packages/flutter_hooks/test/use_ticker_provider_test.dart @@ -1,5 +1,4 @@ import 'package:flutter/foundation.dart'; -import 'package:flutter/scheduler.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; From c0ecf8523b3ceabe7b831f9bad3be68f1f0eefef Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Wed, 10 Nov 2021 13:39:57 +0100 Subject: [PATCH 243/384] Changelog and release --- packages/flutter_hooks/CHANGELOG.md | 6 ++++++ packages/flutter_hooks/pubspec.yaml | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/flutter_hooks/CHANGELOG.md b/packages/flutter_hooks/CHANGELOG.md index 19cacff8..f6a19891 100644 --- a/packages/flutter_hooks/CHANGELOG.md +++ b/packages/flutter_hooks/CHANGELOG.md @@ -1,7 +1,13 @@ +## 0.18.1 + +- Added `useTransformationController`, to create a `TransformationController` (thanks to @shrouxm) +- Added `useAppLifecycleState` and `useOnAppLifecycleStateChange` to interact with `AppLifecycleState` (thanks to @francipvb) + ## 0.18.0 - **Breaking**: `useRef` now receive an initial value as parameter. To migrate, you can change: + ```dart ObjectRef ref = useRef(); ``` diff --git a/packages/flutter_hooks/pubspec.yaml b/packages/flutter_hooks/pubspec.yaml index ee9a6c1d..fe574578 100644 --- a/packages/flutter_hooks/pubspec.yaml +++ b/packages/flutter_hooks/pubspec.yaml @@ -1,7 +1,7 @@ name: flutter_hooks description: A flutter implementation of React hooks. It adds a new kind of widget with enhanced code reuse. homepage: https://github.com/rrousselGit/flutter_hooks -version: 0.18.0 +version: 0.18.1 environment: sdk: ">=2.12.0-0 <3.0.0" From b41363c3d26c8bc6dd432c95303408f0d4232027 Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Wed, 10 Nov 2021 13:40:32 +0100 Subject: [PATCH 244/384] format --- packages/flutter_hooks/lib/src/transformation_controller.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/flutter_hooks/lib/src/transformation_controller.dart b/packages/flutter_hooks/lib/src/transformation_controller.dart index 25d67d78..d563d17d 100644 --- a/packages/flutter_hooks/lib/src/transformation_controller.dart +++ b/packages/flutter_hooks/lib/src/transformation_controller.dart @@ -25,8 +25,8 @@ class _TransformationControllerHook extends Hook { final Matrix4? initialValue; @override - HookState> createState() => - _TransformationControllerHookState(); + HookState> + createState() => _TransformationControllerHookState(); } class _TransformationControllerHookState From 2b1e94066a91c6d83e7fe858208fb6166c644de5 Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Wed, 10 Nov 2021 13:41:14 +0100 Subject: [PATCH 245/384] Bump dart SDK to stable release --- packages/flutter_hooks/pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/flutter_hooks/pubspec.yaml b/packages/flutter_hooks/pubspec.yaml index fe574578..45e402ba 100644 --- a/packages/flutter_hooks/pubspec.yaml +++ b/packages/flutter_hooks/pubspec.yaml @@ -4,7 +4,7 @@ homepage: https://github.com/rrousselGit/flutter_hooks version: 0.18.1 environment: - sdk: ">=2.12.0-0 <3.0.0" + sdk: ">=2.12.0 <3.0.0" flutter: ">=1.20.0" dependencies: From d672828dc4cb3d7dd8ea3e91a262c8c1785d6858 Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Wed, 10 Nov 2021 13:41:46 +0100 Subject: [PATCH 246/384] Remove unused imports --- packages/flutter_hooks/test/use_transformation_controller.dart | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/flutter_hooks/test/use_transformation_controller.dart b/packages/flutter_hooks/test/use_transformation_controller.dart index fee23eb6..2b3aa35f 100644 --- a/packages/flutter_hooks/test/use_transformation_controller.dart +++ b/packages/flutter_hooks/test/use_transformation_controller.dart @@ -1,9 +1,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/scheduler.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'; From 22ead815e712c24e7019965185e33f562da7808e Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Tue, 14 Dec 2021 00:16:01 +0100 Subject: [PATCH 247/384] Add FUNDING.yml --- .github/FUNDING.yml | 1 + 1 file changed, 1 insertion(+) create mode 100644 .github/FUNDING.yml diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 00000000..63c29411 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1 @@ +github: rrousselGit From ce9077118da074f35cba8147f76721190e1a0be7 Mon Sep 17 00:00:00 2001 From: jeiea Date: Thu, 30 Dec 2021 06:32:00 +0900 Subject: [PATCH 248/384] feat: allow null in useListenable (#276) --- packages/flutter_hooks/CHANGELOG.md | 4 +++ packages/flutter_hooks/lib/src/animation.dart | 2 +- .../flutter_hooks/lib/src/listenable.dart | 14 ++++---- packages/flutter_hooks/pubspec.yaml | 4 +-- .../test/use_listenable_test.dart | 35 +++++++++++++++++++ 5 files changed, 49 insertions(+), 10 deletions(-) diff --git a/packages/flutter_hooks/CHANGELOG.md b/packages/flutter_hooks/CHANGELOG.md index f6a19891..a8b64cb4 100644 --- a/packages/flutter_hooks/CHANGELOG.md +++ b/packages/flutter_hooks/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.18.2 + +- Allow null in `useListenable` + ## 0.18.1 - Added `useTransformationController`, to create a `TransformationController` (thanks to @shrouxm) diff --git a/packages/flutter_hooks/lib/src/animation.dart b/packages/flutter_hooks/lib/src/animation.dart index 32ad92d1..28f5774f 100644 --- a/packages/flutter_hooks/lib/src/animation.dart +++ b/packages/flutter_hooks/lib/src/animation.dart @@ -24,7 +24,7 @@ class _UseAnimationStateHook extends _ListenableStateHook { String get debugLabel => 'useAnimation'; @override - Object? get debugValue => (hook.listenable as Animation).value; + Object? get debugValue => (hook.listenable as Animation?)?.value; } /// Creates an [AnimationController] automatically disposed. diff --git a/packages/flutter_hooks/lib/src/listenable.dart b/packages/flutter_hooks/lib/src/listenable.dart index fbfd2227..62d17f79 100644 --- a/packages/flutter_hooks/lib/src/listenable.dart +++ b/packages/flutter_hooks/lib/src/listenable.dart @@ -24,7 +24,7 @@ class _UseValueListenableStateHook extends _ListenableStateHook { String get debugLabel => 'useValueListenable'; @override - Object? get debugValue => (hook.listenable as ValueListenable).value; + Object? get debugValue => (hook.listenable as ValueListenable?)?.value; } /// Subscribes to a [Listenable] and mark the widget as needing build @@ -33,7 +33,7 @@ class _UseValueListenableStateHook extends _ListenableStateHook { /// See also: /// * [Listenable] /// * [useValueListenable], [useAnimation] -T useListenable(T listenable) { +T useListenable(T listenable) { use(_ListenableHook(listenable)); return listenable; } @@ -41,7 +41,7 @@ T useListenable(T listenable) { class _ListenableHook extends Hook { const _ListenableHook(this.listenable); - final Listenable listenable; + final Listenable? listenable; @override _ListenableStateHook createState() => _ListenableStateHook(); @@ -51,15 +51,15 @@ class _ListenableStateHook extends HookState { @override void initHook() { super.initHook(); - hook.listenable.addListener(_listener); + hook.listenable?.addListener(_listener); } @override void didUpdateHook(_ListenableHook oldHook) { super.didUpdateHook(oldHook); if (hook.listenable != oldHook.listenable) { - oldHook.listenable.removeListener(_listener); - hook.listenable.addListener(_listener); + oldHook.listenable?.removeListener(_listener); + hook.listenable?.addListener(_listener); } } @@ -72,7 +72,7 @@ class _ListenableStateHook extends HookState { @override void dispose() { - hook.listenable.removeListener(_listener); + hook.listenable?.removeListener(_listener); } @override diff --git a/packages/flutter_hooks/pubspec.yaml b/packages/flutter_hooks/pubspec.yaml index 45e402ba..ad7ac55d 100644 --- a/packages/flutter_hooks/pubspec.yaml +++ b/packages/flutter_hooks/pubspec.yaml @@ -1,7 +1,7 @@ name: flutter_hooks description: A flutter implementation of React hooks. It adds a new kind of widget with enhanced code reuse. homepage: https://github.com/rrousselGit/flutter_hooks -version: 0.18.1 +version: 0.18.2 environment: sdk: ">=2.12.0 <3.0.0" @@ -14,4 +14,4 @@ dependencies: dev_dependencies: flutter_test: sdk: flutter - mockito: ^5.0.16 \ No newline at end of file + mockito: ^5.0.16 diff --git a/packages/flutter_hooks/test/use_listenable_test.dart b/packages/flutter_hooks/test/use_listenable_test.dart index 88a0616d..a5b77c3a 100644 --- a/packages/flutter_hooks/test/use_listenable_test.dart +++ b/packages/flutter_hooks/test/use_listenable_test.dart @@ -74,4 +74,39 @@ void main() { listenable.dispose(); previousListenable.dispose(); }); + + testWidgets('useListenable should handle null', (tester) async { + ValueNotifier? listenable; + + Future pump() { + return tester.pumpWidget(HookBuilder( + builder: (context) { + useListenable(listenable); + return Container(); + }, + )); + } + + await pump(); + + final element = tester.firstElement(find.byType(HookBuilder)); + expect(element.dirty, false); + + final notifier = ValueNotifier(0); + listenable = notifier; + await pump(); + + // ignore: invalid_use_of_protected_member + expect(listenable.hasListeners, true); + + listenable = null; + await pump(); + + // ignore: invalid_use_of_protected_member + expect(notifier.hasListeners, false); + + await tester.pumpWidget(const SizedBox()); + + notifier.dispose(); + }); } From c814b19553af5ce80ea4848e7817982746830178 Mon Sep 17 00:00:00 2001 From: Mohammed Anas Date: Thu, 3 Feb 2022 17:59:15 +0300 Subject: [PATCH 249/384] Remove extraneous parenthesis (#282) --- packages/flutter_hooks/lib/src/primitives.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/flutter_hooks/lib/src/primitives.dart b/packages/flutter_hooks/lib/src/primitives.dart index fb66146f..b1c2428b 100644 --- a/packages/flutter_hooks/lib/src/primitives.dart +++ b/packages/flutter_hooks/lib/src/primitives.dart @@ -105,7 +105,7 @@ class _MemoizedHookState extends HookState> { /// AnimationController controller; /// Color color; /// -/// useValueChanged(color, (_, __)) { +/// useValueChanged(color, (_, __) { /// controller.forward(); /// }); /// ``` From 23a0a8fc66da913b2344f70bee60e46ee367d341 Mon Sep 17 00:00:00 2001 From: Rendani Gangazhe Date: Sun, 6 Feb 2022 15:56:34 +0100 Subject: [PATCH 250/384] Improve documentation (#281) Co-authored-by: Mohammed Anas Co-authored-by: Remi Rousselet --- README.md | 164 +++++++++--------- packages/flutter_hooks/analysis_options.yaml | 4 +- packages/flutter_hooks/lib/src/animation.dart | 6 +- packages/flutter_hooks/lib/src/async.dart | 16 +- packages/flutter_hooks/lib/src/focus.dart | 2 +- packages/flutter_hooks/lib/src/framework.dart | 90 +++++----- .../flutter_hooks/lib/src/listenable.dart | 8 +- packages/flutter_hooks/lib/src/misc.dart | 12 +- .../lib/src/page_controller.dart | 2 +- .../flutter_hooks/lib/src/primitives.dart | 28 +-- .../lib/src/scroll_controller.dart | 2 +- .../flutter_hooks/lib/src/tab_controller.dart | 2 +- .../lib/src/text_controller.dart | 4 +- .../lib/src/widgets_binding_observer.dart | 4 +- 14 files changed, 171 insertions(+), 173 deletions(-) diff --git a/README.md b/README.md index f3a4be4a..8d169e29 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ A Flutter implementation of React hooks: https://medium.com/@dan_abramov/making-sense-of-react-hooks-fdbde8803889 -Hooks are a new kind of object that manages a `Widget` life-cycles. They exist +Hooks are a new kind of object that manage the life-cycle of a `Widget`. They exist for one reason: increase the code-sharing _between_ widgets by removing duplicates. ## Motivation @@ -59,12 +59,12 @@ class _ExampleState extends State with SingleTickerProviderStateMixin { ``` All widgets that desire to use an `AnimationController` will have to reimplement -almost all of this from scratch, which is of course undesired. +almost all of this logic from scratch, which is of course undesired. Dart mixins can partially solve this issue, but they suffer from other problems: - A given mixin can only be used once per class. -- Mixins and the class shares the same object.\ +- Mixins and the class share the same object.\ This means that if two mixins define a variable under the same name, the result may vary between compilation fails to unknown behavior. @@ -87,20 +87,19 @@ class Example extends HookWidget { } ``` -This code is strictly equivalent to the previous example. It still disposes the +This code is functionally equivalent to the previous example. It still disposes the `AnimationController` and still updates its `duration` when `Example.duration` changes. But you're probably thinking: > Where did all the logic go? -That logic moved into `useAnimationController`, a function included directly in -this library (see [Existing hooks](https://github.com/rrousselGit/flutter_hooks#existing-hooks)). -It is what we call a _Hook_. +That logic has been moved into `useAnimationController`, a function included directly in +this library (see [Existing hooks](https://github.com/rrousselGit/flutter_hooks#existing-hooks)) - It is what we call a _Hook_. -Hooks are a new kind of objects with some specificities: +Hooks are a new kind of object with some specificities: - They can only be used in the `build` method of a widget that mix-in `Hooks`. -- The same hook is reusable an infinite number of times +- The same hook can be reused arbitrarily many times. The following code defines two independent `AnimationController`, and they are correctly preserved when the widget rebuild. @@ -113,20 +112,20 @@ Hooks are a new kind of objects with some specificities: ``` - Hooks are entirely independent of each other and from the widget.\ - This means they can easily be extracted into a package and published on + This means that they can easily be extracted into a package and published on [pub](https://pub.dartlang.org/) for others to use. ## Principle -Similarly to `State`, hooks are stored on the `Element` of a `Widget`. But instead -of having one `State`, the `Element` stores a `List`. Then to use a `Hook`, +Similar to `State`, hooks are stored in the `Element` of a `Widget`. However, instead +of having one `State`, the `Element` stores a `List`. Then in order to use a `Hook`, one must call `Hook.use`. The hook returned by `use` is based on the number of times it has been called. The first call returns the first hook; the second call returns the second hook, -the third returns the third hook, ... +the third call returns the third hook and so on. -If this is still unclear, a naive implementation of hooks is the following: +If this idea is still unclear, a naive implementation of hooks could look as follows: ```dart class HookElement extends Element { @@ -143,8 +142,8 @@ class HookElement extends Element { } ``` -For more explanation of how they are implemented, here's a great article about -how they did it in React: https://medium.com/@ryardley/react-hooks-not-magic-just-arrays-cd4f1857236e +For more explanation of how hooks are implemented, here's a great article about +how is was done in React: https://medium.com/@ryardley/react-hooks-not-magic-just-arrays-cd4f1857236e ## Rules @@ -186,9 +185,9 @@ Widget build(BuildContext context) { ### About hot-reload -Since hooks are obtained from their index, one may think that hot-reload while refactoring will break the application. +Since hooks are obtained from their index, one may think that hot-reloads while refactoring will break the application. -But worry not, `HookWidget` overrides the default hot-reload behavior to work with hooks. Still, there are some situations in which the state of a Hook may get reset. +But worry not, a `HookWidget` overrides the default hot-reload behavior to work with hooks. Still, there are some situations in which the state of a Hook may be reset. Consider the following list of hooks: @@ -198,7 +197,7 @@ useB(0); useC(); ``` -Then consider that after a hot-reload, we edited the parameter of `HookB`: +Then consider that we edited the parameter of `HookB` after performing a hot-reload: ```dart useA(); @@ -206,7 +205,7 @@ useB(42); useC(); ``` -Here everything works fine; all hooks keep their states. +Here everything works fine and all hooks maintain their state. Now consider that we removed `HookB`. We now have: @@ -215,22 +214,22 @@ useA(); useC(); ``` -In this situation, `HookA` keeps its state but `HookC` gets a hard reset. -This happens because when a refactoring is done, all hooks _after_ the first line impacted are disposed of. -Since `HookC` was placed after `HookB`, it got disposed of. +In this situation, `HookA` maintains its state but `HookC` gets hard reset. +This happens because, when a hot-reload is perfomed after refactoring, all hooks _after_ the first line impacted are disposed of. +So, since `HookC` was placed _after_ `HookB`, it will be disposed. -## How to use +## How to create a hook There are two ways to create a hook: - A function - Functions are by far the most common way to write a hook. Thanks to hooks being + Functions are by far the most common way to write hooks. Thanks to hooks being composable by nature, a function will be able to combine other hooks to create - a custom hook. By convention, these functions will be prefixed by `use`. + a more complex custom hook. By convention, these functions will be prefixed by `use`. - The following defines a custom hook that creates a variable and logs its value - on the console whenever the value changes: + The following code defines a custom hook that creates a variable and logs its value + to the console whenever the value changes: ```dart ValueNotifier useLoggedState(BuildContext context, [T initialData]) { @@ -244,10 +243,11 @@ There are two ways to create a hook: - A class - When a hook becomes too complex, it is possible to convert it into a class that extends `Hook`, which can then be used using `Hook.use`.\ - As a class, the hook will look very similar to a `State` and have access to - life-cycles and methods such as `initHook`, `dispose` and `setState` - It is usually a good practice to hide the class under a function as such: + When a hook becomes too complex, it is possible to convert it into a class that extends `Hook` - which can then be used using `Hook.use`.\ + As a class, the hook will look very similar to a `State` class and have access to widget + life-cycle and methods such as `initHook`, `dispose` and `setState`. + + It is usually good practice to hide the class under a function as such: ```dart Result useMyHook(BuildContext context) { @@ -255,7 +255,7 @@ There are two ways to create a hook: } ``` - The following defines a hook that prints the time a `State` has been alive. + The following code defines a hook that prints the total time a `State` has been alive on its dispose. ```dart class _TimeAlive extends Hook { @@ -287,70 +287,68 @@ There are two ways to create a hook: ## Existing hooks -Flutter_hooks comes with a list of reusable hooks already provided. - -They are divided into different kinds: +Flutter_Hooks already comes with a list of reusable hooks which are divided into different kinds: ### Primitives -A set of low-level hooks that interacts with the different life-cycles of a widget +A set of low-level hooks that interact with the different life-cycles of a widget -| name | description | -| ----------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------- | -| [useEffect](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useEffect.html) | Useful for side-effects and optionally canceling them. | -| [useState](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useState.html) | Create variable and subscribes to it. | -| [useMemoized](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useMemoized.html) | Cache the instance of a complex object. | -| [useRef](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useRef.html) | Creates an object that contains a single mutable property. | -| [useCallback](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useCallback.html) | Cache a function instance. | -| [useContext](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useContext.html) | Obtain the `BuildContext` of the building `HookWidget`. | -| [useValueChanged](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useValueChanged.html) | Watches a value and calls a callback whenever the value changed. | +| Name | Description | +| ----------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------| +| [useEffect](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useEffect.html) | Useful for side-effects and optionally canceling them. | +| [useState](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useState.html) | Creates a variable and subscribes to it. | +| [useMemoized](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useMemoized.html) | Caches the instance of a complex object. | +| [useRef](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useRef.html) | Creates an object that contains a single mutable property. | +| [useCallback](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useCallback.html) | Caches a function instance. | +| [useContext](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useContext.html) | Obtains the `BuildContext` of the building `HookWidget`. | +| [useValueChanged](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useValueChanged.html) | Watches a value and triggers a callback whenever its value changed. | -### Object binding +### Object-binding -This category of hooks allows manipulating existing Flutter/Dart objects with hooks. +This category of hooks the manipulation of existing Flutter/Dart objects with hooks. They will take care of creating/updating/disposing an object. -#### dart:async related: +#### dart:async related hooks: -| name | description | -| ------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------- | -| [useStream](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useStream.html) | Subscribes to a `Stream` and return its current state in an `AsyncSnapshot`. | -| [useStreamController](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useStreamController.html) | Creates a `StreamController` automatically disposed. | -| [useFuture](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useFuture.html) | Subscribes to a `Future` and return its current state in an `AsyncSnapshot`. | +| Name | Description | +| ------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------| +| [useStream](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useStream.html) | Subscribes to a `Stream` and returns its current state as an `AsyncSnapshot`. | +| [useStreamController](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useStreamController.html) | Creates a `StreamController` which will automatically be disposed. | +| [useFuture](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useFuture.html) | Subscribes to a `Future` and returns its current state as an `AsyncSnapshot`. | -#### Animation related: +#### Animation related hooks: -| name | description | -| --------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------- | -| [useSingleTickerProvider](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useSingleTickerProvider.html) | Creates a single usage `TickerProvider`. | -| [useAnimationController](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useAnimationController.html) | Creates an `AnimationController` automatically disposed. | -| [useAnimation](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useAnimation.html) | Subscribes to an `Animation` and return its value. | +| Name | Description | +| --------------------------------------------------------------------------------------------------------------------------------- | -----------------------------------------------------------------------| +| [useSingleTickerProvider](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useSingleTickerProvider.html) | Creates a single usage `TickerProvider`. | +| [useAnimationController](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useAnimationController.html) | Creates an `AnimationController` which will be automatically disposed. | +| [useAnimation](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useAnimation.html) | Subscribes to an `Animation` and returns its value. | -#### Listenable related: +#### Listenable related hooks: -| name | description | -| ----------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------- | -| [useListenable](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useListenable.html) | Subscribes to a `Listenable` and mark the widget as needing build whenever the listener is called. | -| [useValueNotifier](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useValueNotifier.html) | Creates a `ValueNotifier` automatically disposed. | -| [useValueListenable](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useValueListenable.html) | Subscribes to a `ValueListenable` and return its value. | +| Name | Description | +| ----------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------| +| [useListenable](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useListenable.html) | Subscribes to a `Listenable` and marks the widget as needing build whenever the listener is called. | +| [useValueNotifier](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useValueNotifier.html) | Creates a `ValueNotifier` which will be automatically disposed. | +| [useValueListenable](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useValueListenable.html) | Subscribes to a `ValueListenable` and return its value. | -#### Misc +#### Misc hooks: A series of hooks with no particular theme. -| name | description | -| ------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------- | -| [useReducer](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useReducer.html) | An alternative to `useState` for more complex states. | -| [usePrevious](https://pub.dartlang.org/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) | Create a `TextEditingController` | -| [useFocusNode](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useFocusNode.html) | Create a `FocusNode` | -| [useTabController](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useTabController.html) | Creates and disposes a `TabController`. | -| [useScrollController](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useScrollController.html) | Creates and disposes a `ScrollController`. | -| [usePageController](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/usePageController.html) | Creates and disposes a `PageController`. | -| [useAppLifecycleState](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useAppLifecycleState.html) | Returns the current `AppLifecycleState` and rebuild the widget on change. | -| [useOnAppLifecycleStateChange](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useOnAppLifecycleStateChange.html) | Listens to `AppLifecycleState` changes and call a callback on change. | -| [useTransformationController](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useTransformationController.html) | Creates and disposes a `TransformationController`. | -| [useIsMounted](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useIsMounted.html) | An equivalent to `State.mounted` for hooks | +| Name | Description | +| ------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------| +| [useReducer](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useReducer.html) | An alternative to `useState` for more complex states. | +| [usePrevious](https://pub.dartlang.org/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.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useFocusNode.html) | Createx a `FocusNode`. | +| [useTabController](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useTabController.html) | Creates and disposes a `TabController`. | +| [useScrollController](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useScrollController.html) | Creates and disposes a `ScrollController`. | +| [usePageController](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/usePageController.html) | Creates and disposes a `PageController`. | +| [useAppLifecycleState](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useAppLifecycleState.html) | Returns the current `AppLifecycleState` and rebuilds the widget on change. | +| [useOnAppLifecycleStateChange](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useOnAppLifecycleStateChange.html) | Listens to `AppLifecycleState` changes and triggers a callback on change. | +| [useTransformationController](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useTransformationController.html) | Creates and disposes a `TransformationController`. | +| [useIsMounted](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useIsMounted.html) | An equivalent to `State.mounted` for hooks. | ## Contributions @@ -367,12 +365,12 @@ For a custom-hook to be merged, you will need to do the following: a large number of people. If your hook is rejected, don't worry! A rejection doesn't mean that it won't - be merged later in the future if more people shows an interest in it. + be merged later in the future if more people show interest in it. In the mean-time, feel free to publish your hook as a package on https://pub.dev. - Write tests for your hook - A hook will not be merged unles fully tested, to avoid breaking it inadvertendly + A hook will not be merged unless fully tested to avoid inadvertendly breaking it in the future. -- Add it to the Readme & write documentation for it. +- Add it to the README and write documentation for it. diff --git a/packages/flutter_hooks/analysis_options.yaml b/packages/flutter_hooks/analysis_options.yaml index 6607ce52..8c76de8f 100644 --- a/packages/flutter_hooks/analysis_options.yaml +++ b/packages/flutter_hooks/analysis_options.yaml @@ -14,7 +14,7 @@ analyzer: linter: rules: - # Uninitianalized late variables are dangerous + # Uninitianalized late variables are dangerous use_late_for_private_fields_and_variables: false # Personal preference. I don't find it more readable @@ -69,4 +69,4 @@ linter: no_default_cases: false # Too verbose - diagnostic_describe_all_properties: false \ No newline at end of file + diagnostic_describe_all_properties: false diff --git a/packages/flutter_hooks/lib/src/animation.dart b/packages/flutter_hooks/lib/src/animation.dart index 28f5774f..be066a9f 100644 --- a/packages/flutter_hooks/lib/src/animation.dart +++ b/packages/flutter_hooks/lib/src/animation.dart @@ -1,6 +1,6 @@ part of 'hooks.dart'; -/// Subscribes to an [Animation] and return its value. +/// Subscribes to an [Animation] and returns its value. /// /// See also: /// * [Animation] @@ -27,13 +27,13 @@ class _UseAnimationStateHook extends _ListenableStateHook { Object? get debugValue => (hook.listenable as Animation?)?.value; } -/// Creates an [AnimationController] automatically disposed. +/// Creates an [AnimationController] and automatically disposes it when necessary. /// /// If no [vsync] is provided, the [TickerProvider] is implicitly obtained using [useSingleTickerProvider]. /// If a [vsync] is specified, changing the instance of [vsync] will result in a call to [AnimationController.resync]. /// It is not possible to switch between implicit and explicit [vsync]. /// -/// Changing the [duration] parameter automatically updates [AnimationController.duration]. +/// Changing the [duration] parameter automatically updates the [AnimationController.duration]. /// /// [initialValue], [lowerBound], [upperBound] and [debugLabel] are ignored after the first call. /// diff --git a/packages/flutter_hooks/lib/src/async.dart b/packages/flutter_hooks/lib/src/async.dart index 90c1809d..a199bc4d 100644 --- a/packages/flutter_hooks/lib/src/async.dart +++ b/packages/flutter_hooks/lib/src/async.dart @@ -1,8 +1,8 @@ part of 'hooks.dart'; -/// Subscribes to a [Future] and return its current state in an [AsyncSnapshot]. +/// Subscribes to a [Future] and returns its current state as an [AsyncSnapshot]. /// -/// * [preserveState] defines if the current value should be preserved when changing +/// * [preserveState] determines if the current value should be preserved when changing /// the [Future] instance. /// /// See also: @@ -39,8 +39,8 @@ class _FutureHook extends Hook> { class _FutureStateHook extends HookState, _FutureHook> { /// 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. + /// calling `setState` from stale callbacks, e.g. after disposal of this state, + /// or after widget reconfiguration to a new [Future]. Object? _activeCallbackIdentity; late AsyncSnapshot _snapshot = initial; @@ -117,10 +117,10 @@ class _FutureStateHook extends HookState, _FutureHook> { Object? get debugValue => _snapshot; } -/// Subscribes to a [Stream] and return its current state in an [AsyncSnapshot]. +/// Subscribes to a [Stream] and returns its current state as an [AsyncSnapshot]. /// -/// * [preserveState] defines if the current value should be preserved when changing -/// the [Future] instance. +/// * [preserveState] determines if the current value should be preserved when changing +/// the [Stream] instance. /// /// See also: /// * [Stream], the object listened. @@ -245,7 +245,7 @@ class _StreamHookState extends HookState, _StreamHook> { String get debugLabel => 'useStream'; } -/// Creates a [StreamController] automatically disposed. +/// Creates a [StreamController] which is automatically disposed when necessary. /// /// See also: /// * [StreamController], the created object diff --git a/packages/flutter_hooks/lib/src/focus.dart b/packages/flutter_hooks/lib/src/focus.dart index fa8b80ae..04ba90f6 100644 --- a/packages/flutter_hooks/lib/src/focus.dart +++ b/packages/flutter_hooks/lib/src/focus.dart @@ -1,6 +1,6 @@ part of 'hooks.dart'; -/// Creates and dispose of a [FocusNode]. +/// Creates an automatically disposed [FocusNode]. /// /// See also: /// - [FocusNode] diff --git a/packages/flutter_hooks/lib/src/framework.dart b/packages/flutter_hooks/lib/src/framework.dart index 97a256b2..f1137f90 100644 --- a/packages/flutter_hooks/lib/src/framework.dart +++ b/packages/flutter_hooks/lib/src/framework.dart @@ -3,15 +3,15 @@ import 'dart:collection'; import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; -/// Wether to behave like in release mode or allow hot-reload for hooks. +/// Whether to behave like in release mode or allow hot-reload for hooks. /// /// `true` by default. It has no impact on release builds. bool debugHotReloadHooksEnabled = true; -/// Register a [Hook] and returns its value +/// Registers a [Hook] and returns its value. /// -/// [use] must be called withing `build` of either [HookWidget] or [StatefulHookWidget], -/// and all calls to [use] must be made unconditionally, always on the same order. +/// [use] must be called within the `build` method of either [HookWidget] or [StatefulHookWidget]. +/// All calls of [use] must be made outside of conditional checks and always in the same order. /// /// See [Hook] for more explanations. // ignore: deprecated_member_use, deprecated_member_use_from_same_package @@ -22,7 +22,7 @@ R use(Hook hook) => Hook.use(hook); /// /// A [Hook] is typically the equivalent of [State] for [StatefulWidget], /// with the notable difference that a [HookWidget] can have more than one [Hook]. -/// A [Hook] is created within the [HookState.build] method of [HookWidget] and the creation +/// A [Hook] is created within the [HookState.build] method of a [HookWidget] and the creation /// must be made unconditionally, always in the same order. /// /// ### Good: @@ -49,19 +49,19 @@ R use(Hook hook) => Hook.use(hook); /// } /// ``` /// -/// The reason for such restriction is that [HookState] are obtained based on their index. +/// The reason for such restrictions is that [HookState] are obtained based on their index. /// So the index must never ever change, or it will lead to undesired behavior. /// -/// ## The usage +/// ## Usage /// -/// [Hook] is powerful tool to reuse [State] logic between multiple [Widget]. +/// [Hook] is a powerful tool which enables the reuse of [State] logic between multiple [Widget]. /// They are used to extract logic that depends on a [Widget] life-cycle (such as [HookState.dispose]). /// /// While mixins are a good candidate too, they do not allow sharing values. A mixin cannot reasonably -/// define a variable, as this can lead to variable conflicts on bigger widgets. +/// define a variable, as this can lead to variable conflicts in bigger widgets. /// -/// Hooks are designed so that they get the benefits of mixins, but are totally independent from each others. -/// This means that hooks can store and expose values without fearing that the name is already taken by another mixin. +/// Hooks are designed so that they get the benefits of mixins, but are totally independent from each other. +/// This means that hooks can store and expose values without needing to check if the name is already taken by another mixin. /// /// ## Example /// @@ -98,9 +98,9 @@ R use(Hook hook) => Hook.use(hook); /// This is undesired because every single widget that wants to use an [AnimationController] will have to /// rewrite this exact piece of code. /// -/// With hooks it is possible to extract that exact piece of code into a reusable one. +/// With hooks, it is possible to extract that exact piece of code into a reusable one. /// -/// This means that with [HookWidget] the following code is equivalent to the previous example: +/// This means that with [HookWidget] the following code is functionally equivalent to the previous example: /// /// ``` /// class Usual extends HookWidget { @@ -112,20 +112,20 @@ R use(Hook hook) => Hook.use(hook); /// } /// ``` /// -/// This is visibly less code then before. But in this example, the `animationController` is still +/// This is visibly less code then before, but in this example, the `animationController` is still /// guaranteed to be disposed when the widget is removed from the tree. /// -/// In fact this has secondary bonus: `duration` is kept updated with the latest value. +/// In fact, this has a secondary bonus: `duration` is kept updated with the latest value. /// If we were to pass a variable as `duration` instead of a constant, then on value change the [AnimationController] will be updated. @immutable abstract class Hook with Diagnosticable { /// Allows subclasses to have a `const` constructor const Hook({this.keys}); - /// Register a [Hook] and returns its value + /// Registers a [Hook] and returns its value. /// - /// [use] must be called withing `build` of either [HookWidget] or [StatefulHookWidget], - /// and all calls to [use] must be made unconditionally, always on the same order. + /// [use] must be called within the `build` method of either [HookWidget] or [StatefulHookWidget]. + /// All calls to [use] must be made outside of conditional statements and always on the same order. /// /// See [Hook] for more explanations. @Deprecated('Use `use` instead of `Hook.use`') @@ -160,7 +160,7 @@ Calling them outside of build method leads to an unstable state and is therefore if (p1 == p2) { return true; } - // is one list is null and the other one isn't, or if they have different size + // if one list is null and the other one isn't, or if they have different sizes if (p1 == null || p2 == null || p1.length != p2.length) { return false; } @@ -178,9 +178,9 @@ Calling them outside of build method leads to an unstable state and is therefore } } - /// Creates the mutable state for this hook linked to its widget creator. + /// Creates the mutable state for this [Hook] linked to its widget creator. /// - /// Subclasses should override this method to return a newly created instance of their associated State subclass: + /// Subclasses should override this method to return a newly created instance of their associated [State] subclass: /// /// ``` /// @override @@ -207,38 +207,38 @@ abstract class HookState> with Diagnosticable { /// Defaults to the last value returned by [build]. Object? get debugValue => _debugLastBuiltValue; - /// A flag to not show [debugValue] in the devtool, for hooks that returns nothing. + /// A flag to prevent showing [debugValue] in the devtool for a [Hook] that returns nothing. bool get debugSkipValue => false; - /// A label used by the devtool to show the state of a hook + /// A label used by the devtool to show the state of a [Hook]. String? get debugLabel => null; - /// Whether the devtool description should skip [debugFillProperties] or not. + /// Whether or not the devtool description should skip [debugFillProperties]. bool get debugHasShortDescription => true; - /// Equivalent of [State.widget] for [HookState] + /// Equivalent of [State.widget] for [HookState]. T get hook => _hook!; T? _hook; - /// Equivalent of [State.initState] for [HookState] + /// Equivalent of [State.initState] for [HookState]. @protected void initHook() {} - /// Equivalent of [State.dispose] for [HookState] + /// Equivalent of [State.dispose] for [HookState]. @protected void dispose() {} - /// Called everytime the [HookState] is requested + /// Called everytime the [HookState] is requested. /// - /// [build] is where an [HookState] may use other hooks. This restriction is made to ensure that hooks are unconditionally always requested + /// [build] is where a [HookState] may use other hooks. This restriction is made to ensure that hooks are always unconditionally requested. @protected R build(BuildContext context); - /// Equivalent of [State.didUpdateWidget] for [HookState] + /// Equivalent of [State.didUpdateWidget] for [HookState]. @protected void didUpdateHook(T oldHook) {} - /// Equivalent of [State.deactivate] for [HookState] + /// Equivalent of [State.deactivate] for [HookState]. void deactivate() {} /// {@macro flutter.widgets.reassemble} @@ -280,7 +280,7 @@ abstract class HookState> with Diagnosticable { assert(_element!.dirty, 'Bad state'); } - /// Equivalent of [State.setState] for [HookState] + /// Equivalent of [State.setState] for [HookState]. @protected void setState(VoidCallback fn) { fn(); @@ -360,7 +360,7 @@ mixin HookElement on ComponentElement { bool _debugIsInitHook = false; bool _debugDidReassemble = false; - /// A read-only list of all hooks available. + /// A read-only list of all available hooks. /// /// In release mode, returns `null`. List? get debugHooks { @@ -554,13 +554,13 @@ Type mismatch between hooks: } } -/// A [Widget] that can use [Hook] +/// A [Widget] that can use a [Hook]. /// -/// It's usage is very similar to [StatelessWidget]. -/// [HookWidget] do not have any life-cycle and implements -/// only a [build] method. +/// Its usage is very similar to [StatelessWidget]. +/// [HookWidget] does not have any life cycle and only implements +/// the [build] method. /// -/// The difference is that it can use [Hook], which allows +/// The difference is that it can use a [Hook], which allows a /// [HookWidget] to store mutable data without implementing a [State]. abstract class HookWidget extends StatelessWidget { /// Initializes [key] for subclasses. @@ -574,11 +574,11 @@ class _StatelessHookElement extends StatelessElement with HookElement { _StatelessHookElement(HookWidget hooks) : super(hooks); } -/// A [StatefulWidget] that can use [Hook] +/// A [StatefulWidget] that can use a [Hook]. /// -/// It's usage is very similar to [StatefulWidget], but use hooks inside [State.build]. +/// Its usage is very similar to that of [StatefulWidget], but uses hooks inside [State.build]. /// -/// The difference is that it can use [Hook], which allows +/// The difference is that it can use a [Hook], which allows a /// [HookWidget] to store mutable data without implementing a [State]. abstract class StatefulHookWidget extends StatefulWidget { /// Initializes [key] for subclasses. @@ -592,7 +592,7 @@ class _StatefulHookElement extends StatefulElement with HookElement { _StatefulHookElement(StatefulHookWidget hooks) : super(hooks); } -/// Obtain the [BuildContext] of the building [HookWidget]. +/// Obtains the [BuildContext] of the building [HookWidget]. BuildContext useContext() { assert( HookElement._currentHookElement != null, @@ -601,7 +601,7 @@ BuildContext useContext() { return HookElement._currentHookElement!; } -/// A [HookWidget] that defer its `build` to a callback +/// A [HookWidget] that delegates its `build` to a callback. class HookBuilder extends HookWidget { /// Creates a widget that delegates its build to a callback. /// @@ -611,9 +611,9 @@ class HookBuilder extends HookWidget { Key? key, }) : super(key: key); - /// The callback used by [HookBuilder] to create a widget. + /// The callback used by [HookBuilder] to create a [Widget]. /// - /// If a [Hook] asks for a rebuild, [builder] will be called again. + /// If a [Hook] requests a rebuild, [builder] will be called again. /// [builder] must not return `null`. final Widget Function(BuildContext context) builder; diff --git a/packages/flutter_hooks/lib/src/listenable.dart b/packages/flutter_hooks/lib/src/listenable.dart index 62d17f79..8b4c214f 100644 --- a/packages/flutter_hooks/lib/src/listenable.dart +++ b/packages/flutter_hooks/lib/src/listenable.dart @@ -1,6 +1,6 @@ part of 'hooks.dart'; -/// Subscribes to a [ValueListenable] and return its value. +/// Subscribes to a [ValueListenable] and returns its value. /// /// See also: /// * [ValueListenable], the created object @@ -27,7 +27,7 @@ class _UseValueListenableStateHook extends _ListenableStateHook { Object? get debugValue => (hook.listenable as ValueListenable?)?.value; } -/// Subscribes to a [Listenable] and mark the widget as needing build +/// Subscribes to a [Listenable] and marks the widget as needing build /// whenever the listener is called. /// /// See also: @@ -82,9 +82,9 @@ class _ListenableStateHook extends HookState { Object? get debugValue => hook.listenable; } -/// Creates a [ValueNotifier] automatically disposed. +/// Creates a [ValueNotifier] that is automatically disposed. /// -/// As opposed to `useState`, this hook do not subscribes to [ValueNotifier]. +/// As opposed to `useState`, this hook does not subscribe to [ValueNotifier]. /// This allows a more granular rebuild. /// /// See also: diff --git a/packages/flutter_hooks/lib/src/misc.dart b/packages/flutter_hooks/lib/src/misc.dart index 0e49618d..6f036a7d 100644 --- a/packages/flutter_hooks/lib/src/misc.dart +++ b/packages/flutter_hooks/lib/src/misc.dart @@ -1,6 +1,6 @@ part of 'hooks.dart'; -/// A state holder that allows mutations by dispatching actions. +/// A store of mutable state that allows mutations by dispatching actions. abstract class Store { /// The current state. /// @@ -21,14 +21,14 @@ typedef Reducer = State Function(State state, Action action); /// An alternative to [useState] for more complex states. /// -/// [useReducer] manages an read only state that can be updated +/// [useReducer] manages a read only instance of state that can be updated /// by dispatching actions which are interpreted by a [Reducer]. /// /// [reducer] is immediately called on first build with [initialAction] /// and [initialState] as parameter. /// /// It is possible to change the [reducer] by calling [useReducer] -/// with a new [Reducer]. +/// with a new [Reducer]. /// /// See also: /// * [Reducer] @@ -129,8 +129,8 @@ class _PreviousHookState extends HookState> { Object? get debugValue => previous; } -/// Runs the callback on every hot reload -/// similar to reassemble in the Stateful widgets +/// Runs the callback on every hot reload, +/// similar to `reassemble` in the stateful widgets. /// /// See also: /// @@ -216,6 +216,6 @@ class _IsMountedHookState extends HookState { Object? get debugValue => _mounted; } -/// Used by [useIsMounted] to allow widgets to determine if the widget is still +/// Used by [useIsMounted] to allow widgets to determine if the [Widget] is still /// in the widget tree or not. typedef IsMounted = bool Function(); diff --git a/packages/flutter_hooks/lib/src/page_controller.dart b/packages/flutter_hooks/lib/src/page_controller.dart index 8e6d1100..ea0d7945 100644 --- a/packages/flutter_hooks/lib/src/page_controller.dart +++ b/packages/flutter_hooks/lib/src/page_controller.dart @@ -1,6 +1,6 @@ part of 'hooks.dart'; -/// Creates and disposes a [PageController]. +/// Creates a [PageController] that will be disposed automatically. /// /// See also: /// - [PageController] diff --git a/packages/flutter_hooks/lib/src/primitives.dart b/packages/flutter_hooks/lib/src/primitives.dart index b1c2428b..905637af 100644 --- a/packages/flutter_hooks/lib/src/primitives.dart +++ b/packages/flutter_hooks/lib/src/primitives.dart @@ -1,10 +1,10 @@ part of 'hooks.dart'; -/// A class that stores a single value; +/// A class that stores a single value. /// /// It is typically created by [useRef]. class ObjectRef { - /// A class that stores a single value; + /// A class that stores a single value. /// /// It is typically created by [useRef]. ObjectRef(this.value); @@ -26,7 +26,7 @@ ObjectRef useRef(T initialValue) { /// Cache a function accross rebuilds based on a list of keys. /// -/// This is syntax sugar for [useMemoized], such that instead of: +/// This is syntax sugar for [useMemoized], so that instead of: /// /// ```dart /// final cachedFunction = useMemoized(() => () { @@ -48,12 +48,12 @@ T useCallback( return useMemoized(() => callback, keys); } -/// Cache the instance of a complex object. +/// Caches the instance of a complex object. /// /// [useMemoized] will immediately call [valueBuilder] on first call and store its result. -/// Later, when [HookWidget] rebuilds, the call to [useMemoized] will return the previously created instance without calling [valueBuilder]. +/// Later, when the [HookWidget] rebuilds, the call to [useMemoized] will return the previously created instance without calling [valueBuilder]. /// -/// A later call of [useMemoized] with different [keys] will call [useMemoized] again to create a new instance. +/// A subsequent call of [useMemoized] with different [keys] will call [useMemoized] again to create a new instance. T useMemoized( T Function() valueBuilder, [ List keys = const [], @@ -90,7 +90,7 @@ class _MemoizedHookState extends HookState> { String get debugLabel => 'useMemoized<$T>'; } -/// Watches a value and calls a callback whenever the value changed. +/// Watches a value and triggers a callback whenever the value changed. /// /// [useValueChanged] takes a [valueChange] callback and calls it whenever [value] changed. /// [valueChange] will _not_ be called on the first [useValueChanged] call. @@ -172,8 +172,8 @@ typedef Dispose = void Function(); /// By default [effect] is called on every `build` call, unless [keys] is specified. /// In which case, [effect] is called once on the first [useEffect] call and whenever something within [keys] change/ /// -/// The following example call [useEffect] to subscribes to a [Stream] and cancel the subscription when the widget is disposed. -/// ALso if the [Stream] change, it will cancel the listening on the previous [Stream] and listen to the new one. +/// The following example call [useEffect] to subscribes to a [Stream] and cancels the subscription when the widget is disposed. +/// Also if the [Stream] changes, it will cancel the listening on the previous [Stream] and listen to the new one. /// /// ```dart /// Stream stream; @@ -183,7 +183,7 @@ typedef Dispose = void Function(); /// // or if the callback is called again. /// return subscription.cancel; /// }, -/// // when the stream change, useEffect will call the callback again. +/// // when the stream changes, useEffect will call the callback again. /// [stream], /// ); /// ``` @@ -239,11 +239,11 @@ class _EffectHookState extends HookState { /// Creates a variable and subscribes to it. /// /// Whenever [ValueNotifier.value] updates, it will mark the caller [HookWidget] -/// as needing build. -/// On first call, inits [ValueNotifier] to [initialData]. [initialData] is ignored +/// as needing a build. +/// On the first call, it initializes [ValueNotifier] to [initialData]. [initialData] is ignored /// on subsequent calls. /// -/// The following example showcase a basic counter application. +/// The following example showcases a basic counter application: /// /// ```dart /// class Counter extends HookWidget { @@ -252,7 +252,7 @@ class _EffectHookState extends HookState { /// final counter = useState(0); /// /// return GestureDetector( -/// // automatically triggers a rebuild of Counter widget +/// // automatically triggers a rebuild of the Counter widget /// onTap: () => counter.value++, /// child: Text(counter.value.toString()), /// ); diff --git a/packages/flutter_hooks/lib/src/scroll_controller.dart b/packages/flutter_hooks/lib/src/scroll_controller.dart index 5712654a..7a6a539f 100644 --- a/packages/flutter_hooks/lib/src/scroll_controller.dart +++ b/packages/flutter_hooks/lib/src/scroll_controller.dart @@ -1,6 +1,6 @@ part of 'hooks.dart'; -/// Creates and disposes a [ScrollController]. +/// Creates [ScrollController] that will be disposed automatically. /// /// See also: /// - [ScrollController] diff --git a/packages/flutter_hooks/lib/src/tab_controller.dart b/packages/flutter_hooks/lib/src/tab_controller.dart index b73af414..5aba96c3 100644 --- a/packages/flutter_hooks/lib/src/tab_controller.dart +++ b/packages/flutter_hooks/lib/src/tab_controller.dart @@ -1,6 +1,6 @@ part of 'hooks.dart'; -/// Creates and disposes a [TabController]. +/// Creates a [TabController] that will be disposed automatically. /// /// See also: /// - [TabController] diff --git a/packages/flutter_hooks/lib/src/text_controller.dart b/packages/flutter_hooks/lib/src/text_controller.dart index 1d5e52fb..e8f8310d 100644 --- a/packages/flutter_hooks/lib/src/text_controller.dart +++ b/packages/flutter_hooks/lib/src/text_controller.dart @@ -24,12 +24,12 @@ class _TextEditingControllerHookCreator { /// Creates a [TextEditingController], either via an initial text or an initial /// [TextEditingValue]. /// -/// To use a [TextEditingController] with an optional initial text, use +/// To use a [TextEditingController] with an optional initial text, use: /// ```dart /// final controller = useTextEditingController(text: 'initial text'); /// ``` /// -/// To use a [TextEditingController] with an optional initial value, use +/// To use a [TextEditingController] with an optional initial value, use: /// ```dart /// final controller = useTextEditingController /// .fromValue(TextEditingValue.empty); diff --git a/packages/flutter_hooks/lib/src/widgets_binding_observer.dart b/packages/flutter_hooks/lib/src/widgets_binding_observer.dart index 09ab34aa..70ec3d10 100644 --- a/packages/flutter_hooks/lib/src/widgets_binding_observer.dart +++ b/packages/flutter_hooks/lib/src/widgets_binding_observer.dart @@ -1,12 +1,12 @@ part of 'hooks.dart'; -/// A callback to call when the app lifecycle changes. +/// A callback triggered when the app life cycle changes. typedef LifecycleCallback = FutureOr Function( AppLifecycleState? previous, AppLifecycleState current, ); -/// Returns the current [AppLifecycleState] value and rebuild the widget when it changes. +/// Returns the current [AppLifecycleState] value and rebuilds the widget when it changes. AppLifecycleState? useAppLifecycleState() { return use(const _AppLifecycleHook(rebuildOnChange: true)); } From ed0fad3245d3d999bee12afb7ddecb355623b87c Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Sun, 6 Feb 2022 15:58:30 +0100 Subject: [PATCH 251/384] 0.18.2+1 --- packages/flutter_hooks/CHANGELOG.md | 4 ++++ packages/flutter_hooks/pubspec.yaml | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/flutter_hooks/CHANGELOG.md b/packages/flutter_hooks/CHANGELOG.md index a8b64cb4..f3320f50 100644 --- a/packages/flutter_hooks/CHANGELOG.md +++ b/packages/flutter_hooks/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.18.2+1 + +Improved the documentation (thanks to @Renni771) + ## 0.18.2 - Allow null in `useListenable` diff --git a/packages/flutter_hooks/pubspec.yaml b/packages/flutter_hooks/pubspec.yaml index ad7ac55d..79526b0a 100644 --- a/packages/flutter_hooks/pubspec.yaml +++ b/packages/flutter_hooks/pubspec.yaml @@ -1,7 +1,7 @@ name: flutter_hooks description: A flutter implementation of React hooks. It adds a new kind of widget with enhanced code reuse. homepage: https://github.com/rrousselGit/flutter_hooks -version: 0.18.2 +version: 0.18.2+1 environment: sdk: ">=2.12.0 <3.0.0" From e1028fcacadb6ef3769902807694c0e1104bf253 Mon Sep 17 00:00:00 2001 From: ronnieeeeee <60412984+ronnieeeeee@users.noreply.github.com> Date: Wed, 23 Feb 2022 19:40:22 +0900 Subject: [PATCH 252/384] Remove unused parameter in README.md (#284) --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 8d169e29..f81c344c 100644 --- a/README.md +++ b/README.md @@ -232,7 +232,7 @@ There are two ways to create a hook: to the console whenever the value changes: ```dart - ValueNotifier useLoggedState(BuildContext context, [T initialData]) { + ValueNotifier useLoggedState([T initialData]) { final result = useState(initialData); useValueChanged(result.value, (_, __) { print(result.value); @@ -250,7 +250,7 @@ There are two ways to create a hook: It is usually good practice to hide the class under a function as such: ```dart - Result useMyHook(BuildContext context) { + Result useMyHook() { return use(const _TimeAlive()); } ``` From 9a5f1bf122bcd5c95c8b345bbeb7635d7cab3f0b Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Wed, 6 Apr 2022 09:07:43 +0200 Subject: [PATCH 253/384] add dependabot.yaml --- .github/dependabot.yaml | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .github/dependabot.yaml diff --git a/.github/dependabot.yaml b/.github/dependabot.yaml new file mode 100644 index 00000000..39bd9ac1 --- /dev/null +++ b/.github/dependabot.yaml @@ -0,0 +1,7 @@ +version: 2 +enable-beta-ecosystems: true +updates: + - package-ecosystem: "pub" + directory: "/" + schedule: + interval: "weekly" \ No newline at end of file From fc5d9544a5f916c2f7ef1e177ac582f7c5a9d06b Mon Sep 17 00:00:00 2001 From: Kevin DeLorey Date: Thu, 7 Apr 2022 13:05:16 -0600 Subject: [PATCH 254/384] Add `onKeyEvent` to `useFocusNode` (#290) --- packages/flutter_hooks/lib/src/focus.dart | 9 ++++++++- .../flutter_hooks/test/use_focus_node_test.dart | 17 ++++++++++++++++- 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/packages/flutter_hooks/lib/src/focus.dart b/packages/flutter_hooks/lib/src/focus.dart index 04ba90f6..5b22582e 100644 --- a/packages/flutter_hooks/lib/src/focus.dart +++ b/packages/flutter_hooks/lib/src/focus.dart @@ -7,6 +7,7 @@ part of 'hooks.dart'; FocusNode useFocusNode({ String? debugLabel, FocusOnKeyCallback? onKey, + FocusOnKeyEventCallback? onKeyEvent, bool skipTraversal = false, bool canRequestFocus = true, bool descendantsAreFocusable = true, @@ -15,6 +16,7 @@ FocusNode useFocusNode({ _FocusNodeHook( debugLabel: debugLabel, onKey: onKey, + onKeyEvent: onKeyEvent, skipTraversal: skipTraversal, canRequestFocus: canRequestFocus, descendantsAreFocusable: descendantsAreFocusable, @@ -26,6 +28,7 @@ class _FocusNodeHook extends Hook { const _FocusNodeHook({ this.debugLabel, this.onKey, + this.onKeyEvent, required this.skipTraversal, required this.canRequestFocus, required this.descendantsAreFocusable, @@ -33,6 +36,7 @@ class _FocusNodeHook extends Hook { final String? debugLabel; final FocusOnKeyCallback? onKey; + final FocusOnKeyEventCallback? onKeyEvent; final bool skipTraversal; final bool canRequestFocus; final bool descendantsAreFocusable; @@ -47,6 +51,7 @@ class _FocusNodeHookState extends HookState { late final FocusNode _focusNode = FocusNode( debugLabel: hook.debugLabel, onKey: hook.onKey, + onKeyEvent: hook.onKeyEvent, skipTraversal: hook.skipTraversal, canRequestFocus: hook.canRequestFocus, descendantsAreFocusable: hook.descendantsAreFocusable, @@ -58,7 +63,9 @@ class _FocusNodeHookState extends HookState { ..debugLabel = hook.debugLabel ..skipTraversal = hook.skipTraversal ..canRequestFocus = hook.canRequestFocus - ..descendantsAreFocusable = hook.descendantsAreFocusable; + ..descendantsAreFocusable = hook.descendantsAreFocusable + ..onKey = hook.onKey + ..onKeyEvent = hook.onKeyEvent; } @override diff --git a/packages/flutter_hooks/test/use_focus_node_test.dart b/packages/flutter_hooks/test/use_focus_node_test.dart index 420d2aba..8e06beb4 100644 --- a/packages/flutter_hooks/test/use_focus_node_test.dart +++ b/packages/flutter_hooks/test/use_focus_node_test.dart @@ -84,12 +84,16 @@ void main() { KeyEventResult onKey(FocusNode node, RawKeyEvent event) => KeyEventResult.ignored; + KeyEventResult onKeyEvent(FocusNode node, KeyEvent event) => + KeyEventResult.ignored; + late FocusNode focusNode; await tester.pumpWidget( HookBuilder(builder: (_) { focusNode = useFocusNode( debugLabel: 'Foo', onKey: onKey, + onKeyEvent: onKeyEvent, skipTraversal: true, canRequestFocus: false, descendantsAreFocusable: false, @@ -100,6 +104,7 @@ void main() { expect(focusNode.debugLabel, 'Foo'); expect(focusNode.onKey, onKey); + expect(focusNode.onKeyEvent, onKeyEvent); expect(focusNode.skipTraversal, true); expect(focusNode.canRequestFocus, false); expect(focusNode.descendantsAreFocusable, false); @@ -111,16 +116,23 @@ void main() { KeyEventResult onKey2(FocusNode node, RawKeyEvent event) => KeyEventResult.ignored; + KeyEventResult onKeyEvent(FocusNode node, KeyEvent event) => + KeyEventResult.ignored; + KeyEventResult onKeyEvent2(FocusNode node, KeyEvent event) => + KeyEventResult.ignored; + late FocusNode focusNode; await tester.pumpWidget( HookBuilder(builder: (_) { focusNode = useFocusNode( debugLabel: 'Foo', onKey: onKey, + onKeyEvent: onKeyEvent, skipTraversal: true, canRequestFocus: false, descendantsAreFocusable: false, ); + return Container(); }), ); @@ -130,12 +142,15 @@ void main() { focusNode = useFocusNode( debugLabel: 'Bar', onKey: onKey2, + onKeyEvent: onKeyEvent2, ); + return Container(); }), ); - expect(focusNode.onKey, onKey, reason: 'onKey has no setter'); + expect(focusNode.onKey, onKey2); + expect(focusNode.onKeyEvent, onKeyEvent2); expect(focusNode.debugLabel, 'Bar'); expect(focusNode.skipTraversal, false); expect(focusNode.canRequestFocus, true); From c1305dbb0fc0e6c0f3eb916d362b767fa302a4c6 Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Thu, 7 Apr 2022 21:05:59 +0200 Subject: [PATCH 255/384] Changelog --- packages/flutter_hooks/CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/flutter_hooks/CHANGELOG.md b/packages/flutter_hooks/CHANGELOG.md index f3320f50..d7258711 100644 --- a/packages/flutter_hooks/CHANGELOG.md +++ b/packages/flutter_hooks/CHANGELOG.md @@ -1,3 +1,7 @@ +## [Unreleased] + +Added `onKeyEvent` to `useFocusNode` (thanks to @kdelorey) + ## 0.18.2+1 Improved the documentation (thanks to @Renni771) From 2acf85300de7f5fe36e8f81e699318e3f88c6b86 Mon Sep 17 00:00:00 2001 From: Ronnie <60412984+ronnieeeeee@users.noreply.github.com> Date: Fri, 8 Apr 2022 04:15:32 +0900 Subject: [PATCH 256/384] Add useListenableMap (#292) Co-authored-by: Remi Rousselet --- README.md | 19 +++-- packages/flutter_hooks/lib/src/hooks.dart | 1 + .../lib/src/listenable_selector.dart | 81 ++++++++++++++++++ .../test/use_listenable_selectror_test.dart | 85 +++++++++++++++++++ 4 files changed, 177 insertions(+), 9 deletions(-) create mode 100644 packages/flutter_hooks/lib/src/listenable_selector.dart create mode 100644 packages/flutter_hooks/test/use_listenable_selectror_test.dart diff --git a/README.md b/README.md index f81c344c..a9031c47 100644 --- a/README.md +++ b/README.md @@ -294,7 +294,7 @@ Flutter_Hooks already comes with a list of reusable hooks which are divided into A set of low-level hooks that interact with the different life-cycles of a widget | Name | Description | -| ----------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------| +| ----------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------- | | [useEffect](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useEffect.html) | Useful for side-effects and optionally canceling them. | | [useState](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useState.html) | Creates a variable and subscribes to it. | | [useMemoized](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useMemoized.html) | Caches the instance of a complex object. | @@ -311,7 +311,7 @@ They will take care of creating/updating/disposing an object. #### dart:async related hooks: | Name | Description | -| ------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------| +| ------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------- | | [useStream](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useStream.html) | Subscribes to a `Stream` and returns its current state as an `AsyncSnapshot`. | | [useStreamController](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useStreamController.html) | Creates a `StreamController` which will automatically be disposed. | | [useFuture](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useFuture.html) | Subscribes to a `Future` and returns its current state as an `AsyncSnapshot`. | @@ -319,25 +319,26 @@ They will take care of creating/updating/disposing an object. #### Animation related hooks: | Name | Description | -| --------------------------------------------------------------------------------------------------------------------------------- | -----------------------------------------------------------------------| +| --------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------- | | [useSingleTickerProvider](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useSingleTickerProvider.html) | Creates a single usage `TickerProvider`. | | [useAnimationController](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useAnimationController.html) | Creates an `AnimationController` which will be automatically disposed. | | [useAnimation](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useAnimation.html) | Subscribes to an `Animation` and returns its value. | #### Listenable related hooks: -| Name | Description | -| ----------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------| -| [useListenable](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useListenable.html) | Subscribes to a `Listenable` and marks the widget as needing build whenever the listener is called. | -| [useValueNotifier](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useValueNotifier.html) | Creates a `ValueNotifier` which will be automatically disposed. | -| [useValueListenable](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useValueListenable.html) | Subscribes to a `ValueListenable` and return its value. | +| Name | Description | +| ----------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | +| [useListenable](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useListenable.html) | Subscribes to a `Listenable` and marks the widget as needing build whenever the listener is called. | +| [useListenableSelector](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useListenableSelector.html) | An alternative to [useListenable] for listening to a [Listenable], with the added benefit of rebuilding the widget only if a certain value has changed. | +| [useValueNotifier](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useValueNotifier.html) | Creates a `ValueNotifier` which will be automatically disposed. | +| [useValueListenable](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useValueListenable.html) | Subscribes to a `ValueListenable` and return its value. | #### Misc hooks: A series of hooks with no particular theme. | Name | Description | -| ------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------| +| ------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------- | | [useReducer](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useReducer.html) | An alternative to `useState` for more complex states. | | [usePrevious](https://pub.dartlang.org/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`. | diff --git a/packages/flutter_hooks/lib/src/hooks.dart b/packages/flutter_hooks/lib/src/hooks.dart index 889a03ea..a102f8e4 100644 --- a/packages/flutter_hooks/lib/src/hooks.dart +++ b/packages/flutter_hooks/lib/src/hooks.dart @@ -19,3 +19,4 @@ part 'scroll_controller.dart'; part 'page_controller.dart'; part 'widgets_binding_observer.dart'; part 'transformation_controller.dart'; +part 'listenable_selector.dart'; diff --git a/packages/flutter_hooks/lib/src/listenable_selector.dart b/packages/flutter_hooks/lib/src/listenable_selector.dart new file mode 100644 index 00000000..05cd350f --- /dev/null +++ b/packages/flutter_hooks/lib/src/listenable_selector.dart @@ -0,0 +1,81 @@ +part of 'hooks.dart'; + +/// An alternative to [useListenable] for listening to a [Listenable], with the +/// added benefit of rebuilding the widget only if a certain value has changed. +/// +/// [useListenableSelector] will return the result of the callback. +/// And whenever the listenable notify its listeners, the callback will be +/// re-executed. +/// Then, if the value returned has changed, the widget will rebuild. Otherwise, +/// the widget will ignore the [Listenable] update. +/// +/// The following example uses [useListenableSelector] to listen to a +/// [TextEditingController], yet rebuild the widget only when the input changes +/// between empty and not empty. +/// Whereas if we used [useListenable], the widget would've rebuilt everytime +/// the user types a character. +/// +/// ```dart +/// class Example extends HookWidget { +/// @override +/// Widget build(BuildContext context) { +/// final listenable = useTextEditingController(); +/// final bool textIsEmpty = +/// useListenableSelector(listenable, () => listenable.text.isEmpty); +/// return Column( +/// children: [ +/// TextField(controller: listenable), +/// ElevatedButton( +/// // if textIsEmpty is false, the button is disabled. +/// onPressed: textIsEmpty ? null : () => print("Pressed!"), +/// child: Text("Button")), +/// ], +/// ); +/// } +/// } +/// ``` +R useListenableSelector(Listenable listenable, R Function() callback) { + return use(_ListenableSelectorHook(listenable, callback)); +} + +class _ListenableSelectorHook extends Hook { + const _ListenableSelectorHook(this.listenable, this.callback); + + final Listenable listenable; + final R Function() callback; + + @override + _ListenableSelectorHookState createState() => + _ListenableSelectorHookState(); +} + +class _ListenableSelectorHookState + extends HookState> { + late R _state = hook.callback(); + + @override + void initHook() { + super.initHook(); + hook.listenable.addListener(_listener); + } + + @override + R build(BuildContext context) => _state; + + void _listener() { + final result = hook.callback(); + if (_state != result) { + setState(() { + _state = result; + }); + } + } + + @override + void dispose() { + hook.listenable.removeListener(_listener); + } + + @override + String get debugLabel => 'useListenableSelector<$R>'; +} diff --git a/packages/flutter_hooks/test/use_listenable_selectror_test.dart b/packages/flutter_hooks/test/use_listenable_selectror_test.dart new file mode 100644 index 00000000..79a582c9 --- /dev/null +++ b/packages/flutter_hooks/test/use_listenable_selectror_test.dart @@ -0,0 +1,85 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.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) { + final listenable = ValueNotifier(42); + useListenableSelector(listenable, () => listenable.value.isOdd); + return const SizedBox(); + }, + ), + ); + + final element = tester.element(find.byType(HookBuilder)); + + expect( + element + .toDiagnosticsNode(style: DiagnosticsTreeStyle.offstage) + .toStringDeep(), + equalsIgnoringHashCodes( + 'HookBuilder\n' + ' │ useListenableSelector: false\n' + ' └SizedBox(renderObject: RenderConstrainedBox#00000)\n' + '', + ), + ); + }); + + testWidgets('useListenableSelector', (tester) async { + late HookElement element; + late final listenable = ValueNotifier(42); + late bool isOdd; + + Future pump() { + return tester.pumpWidget( + HookBuilder( + builder: (context) { + element = context as HookElement; + isOdd = + useListenableSelector(listenable, () => listenable.value.isOdd); + return Container(); + }, + ), + ); + } + + await pump(); + // ignore: invalid_use_of_protected_member + expect(listenable.hasListeners, true); + expect(listenable.value, 42); + expect(isOdd, false); + expect(element.dirty, false); + + listenable.value++; + expect(element.dirty, true); + await tester.pump(); + expect(listenable.value, 43); + expect(isOdd, true); + expect(element.dirty, false); + + listenable.value = listenable.value + 2; + expect(element.dirty, false); + await tester.pump(); + expect(listenable.value, 45); + expect(isOdd, true); + expect(element.dirty, false); + + listenable.value++; + expect(element.dirty, true); + await tester.pump(); + expect(listenable.value, 46); + expect(isOdd, false); + expect(element.dirty, false); + + await tester.pumpWidget(const SizedBox()); + // ignore: invalid_use_of_protected_member + expect(listenable.hasListeners, false); + }); +} From 8d5c732c553adbf0aaec689a1b73a53c543bff55 Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Thu, 7 Apr 2022 21:21:21 +0200 Subject: [PATCH 257/384] Revert "Add useListenableMap" (#293) --- README.md | 19 ++--- packages/flutter_hooks/lib/src/hooks.dart | 1 - .../lib/src/listenable_selector.dart | 81 ------------------ .../test/use_listenable_selectror_test.dart | 85 ------------------- 4 files changed, 9 insertions(+), 177 deletions(-) delete mode 100644 packages/flutter_hooks/lib/src/listenable_selector.dart delete mode 100644 packages/flutter_hooks/test/use_listenable_selectror_test.dart diff --git a/README.md b/README.md index a9031c47..f81c344c 100644 --- a/README.md +++ b/README.md @@ -294,7 +294,7 @@ Flutter_Hooks already comes with a list of reusable hooks which are divided into A set of low-level hooks that interact with the different life-cycles of a widget | Name | Description | -| ----------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------- | +| ----------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------| | [useEffect](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useEffect.html) | Useful for side-effects and optionally canceling them. | | [useState](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useState.html) | Creates a variable and subscribes to it. | | [useMemoized](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useMemoized.html) | Caches the instance of a complex object. | @@ -311,7 +311,7 @@ They will take care of creating/updating/disposing an object. #### dart:async related hooks: | Name | Description | -| ------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------- | +| ------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------| | [useStream](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useStream.html) | Subscribes to a `Stream` and returns its current state as an `AsyncSnapshot`. | | [useStreamController](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useStreamController.html) | Creates a `StreamController` which will automatically be disposed. | | [useFuture](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useFuture.html) | Subscribes to a `Future` and returns its current state as an `AsyncSnapshot`. | @@ -319,26 +319,25 @@ They will take care of creating/updating/disposing an object. #### Animation related hooks: | Name | Description | -| --------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------- | +| --------------------------------------------------------------------------------------------------------------------------------- | -----------------------------------------------------------------------| | [useSingleTickerProvider](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useSingleTickerProvider.html) | Creates a single usage `TickerProvider`. | | [useAnimationController](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useAnimationController.html) | Creates an `AnimationController` which will be automatically disposed. | | [useAnimation](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useAnimation.html) | Subscribes to an `Animation` and returns its value. | #### Listenable related hooks: -| Name | Description | -| ----------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | -| [useListenable](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useListenable.html) | Subscribes to a `Listenable` and marks the widget as needing build whenever the listener is called. | -| [useListenableSelector](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useListenableSelector.html) | An alternative to [useListenable] for listening to a [Listenable], with the added benefit of rebuilding the widget only if a certain value has changed. | -| [useValueNotifier](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useValueNotifier.html) | Creates a `ValueNotifier` which will be automatically disposed. | -| [useValueListenable](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useValueListenable.html) | Subscribes to a `ValueListenable` and return its value. | +| Name | Description | +| ----------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------| +| [useListenable](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useListenable.html) | Subscribes to a `Listenable` and marks the widget as needing build whenever the listener is called. | +| [useValueNotifier](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useValueNotifier.html) | Creates a `ValueNotifier` which will be automatically disposed. | +| [useValueListenable](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useValueListenable.html) | Subscribes to a `ValueListenable` and return its value. | #### Misc hooks: A series of hooks with no particular theme. | Name | Description | -| ------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------- | +| ------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------| | [useReducer](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useReducer.html) | An alternative to `useState` for more complex states. | | [usePrevious](https://pub.dartlang.org/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`. | diff --git a/packages/flutter_hooks/lib/src/hooks.dart b/packages/flutter_hooks/lib/src/hooks.dart index a102f8e4..889a03ea 100644 --- a/packages/flutter_hooks/lib/src/hooks.dart +++ b/packages/flutter_hooks/lib/src/hooks.dart @@ -19,4 +19,3 @@ part 'scroll_controller.dart'; part 'page_controller.dart'; part 'widgets_binding_observer.dart'; part 'transformation_controller.dart'; -part 'listenable_selector.dart'; diff --git a/packages/flutter_hooks/lib/src/listenable_selector.dart b/packages/flutter_hooks/lib/src/listenable_selector.dart deleted file mode 100644 index 05cd350f..00000000 --- a/packages/flutter_hooks/lib/src/listenable_selector.dart +++ /dev/null @@ -1,81 +0,0 @@ -part of 'hooks.dart'; - -/// An alternative to [useListenable] for listening to a [Listenable], with the -/// added benefit of rebuilding the widget only if a certain value has changed. -/// -/// [useListenableSelector] will return the result of the callback. -/// And whenever the listenable notify its listeners, the callback will be -/// re-executed. -/// Then, if the value returned has changed, the widget will rebuild. Otherwise, -/// the widget will ignore the [Listenable] update. -/// -/// The following example uses [useListenableSelector] to listen to a -/// [TextEditingController], yet rebuild the widget only when the input changes -/// between empty and not empty. -/// Whereas if we used [useListenable], the widget would've rebuilt everytime -/// the user types a character. -/// -/// ```dart -/// class Example extends HookWidget { -/// @override -/// Widget build(BuildContext context) { -/// final listenable = useTextEditingController(); -/// final bool textIsEmpty = -/// useListenableSelector(listenable, () => listenable.text.isEmpty); -/// return Column( -/// children: [ -/// TextField(controller: listenable), -/// ElevatedButton( -/// // if textIsEmpty is false, the button is disabled. -/// onPressed: textIsEmpty ? null : () => print("Pressed!"), -/// child: Text("Button")), -/// ], -/// ); -/// } -/// } -/// ``` -R useListenableSelector(Listenable listenable, R Function() callback) { - return use(_ListenableSelectorHook(listenable, callback)); -} - -class _ListenableSelectorHook extends Hook { - const _ListenableSelectorHook(this.listenable, this.callback); - - final Listenable listenable; - final R Function() callback; - - @override - _ListenableSelectorHookState createState() => - _ListenableSelectorHookState(); -} - -class _ListenableSelectorHookState - extends HookState> { - late R _state = hook.callback(); - - @override - void initHook() { - super.initHook(); - hook.listenable.addListener(_listener); - } - - @override - R build(BuildContext context) => _state; - - void _listener() { - final result = hook.callback(); - if (_state != result) { - setState(() { - _state = result; - }); - } - } - - @override - void dispose() { - hook.listenable.removeListener(_listener); - } - - @override - String get debugLabel => 'useListenableSelector<$R>'; -} diff --git a/packages/flutter_hooks/test/use_listenable_selectror_test.dart b/packages/flutter_hooks/test/use_listenable_selectror_test.dart deleted file mode 100644 index 79a582c9..00000000 --- a/packages/flutter_hooks/test/use_listenable_selectror_test.dart +++ /dev/null @@ -1,85 +0,0 @@ -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/widgets.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) { - final listenable = ValueNotifier(42); - useListenableSelector(listenable, () => listenable.value.isOdd); - return const SizedBox(); - }, - ), - ); - - final element = tester.element(find.byType(HookBuilder)); - - expect( - element - .toDiagnosticsNode(style: DiagnosticsTreeStyle.offstage) - .toStringDeep(), - equalsIgnoringHashCodes( - 'HookBuilder\n' - ' │ useListenableSelector: false\n' - ' └SizedBox(renderObject: RenderConstrainedBox#00000)\n' - '', - ), - ); - }); - - testWidgets('useListenableSelector', (tester) async { - late HookElement element; - late final listenable = ValueNotifier(42); - late bool isOdd; - - Future pump() { - return tester.pumpWidget( - HookBuilder( - builder: (context) { - element = context as HookElement; - isOdd = - useListenableSelector(listenable, () => listenable.value.isOdd); - return Container(); - }, - ), - ); - } - - await pump(); - // ignore: invalid_use_of_protected_member - expect(listenable.hasListeners, true); - expect(listenable.value, 42); - expect(isOdd, false); - expect(element.dirty, false); - - listenable.value++; - expect(element.dirty, true); - await tester.pump(); - expect(listenable.value, 43); - expect(isOdd, true); - expect(element.dirty, false); - - listenable.value = listenable.value + 2; - expect(element.dirty, false); - await tester.pump(); - expect(listenable.value, 45); - expect(isOdd, true); - expect(element.dirty, false); - - listenable.value++; - expect(element.dirty, true); - await tester.pump(); - expect(listenable.value, 46); - expect(isOdd, false); - expect(element.dirty, false); - - await tester.pumpWidget(const SizedBox()); - // ignore: invalid_use_of_protected_member - expect(listenable.hasListeners, false); - }); -} From 5a035547eed730c0b121b1f7573fb0f53ed4730e Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Thu, 7 Apr 2022 21:22:12 +0200 Subject: [PATCH 258/384] 0.18.3 --- packages/flutter_hooks/CHANGELOG.md | 2 +- packages/flutter_hooks/pubspec.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/flutter_hooks/CHANGELOG.md b/packages/flutter_hooks/CHANGELOG.md index d7258711..61b489b1 100644 --- a/packages/flutter_hooks/CHANGELOG.md +++ b/packages/flutter_hooks/CHANGELOG.md @@ -1,4 +1,4 @@ -## [Unreleased] +## 0.18.3 Added `onKeyEvent` to `useFocusNode` (thanks to @kdelorey) diff --git a/packages/flutter_hooks/pubspec.yaml b/packages/flutter_hooks/pubspec.yaml index 79526b0a..8912db2d 100644 --- a/packages/flutter_hooks/pubspec.yaml +++ b/packages/flutter_hooks/pubspec.yaml @@ -1,7 +1,7 @@ name: flutter_hooks description: A flutter implementation of React hooks. It adds a new kind of widget with enhanced code reuse. homepage: https://github.com/rrousselGit/flutter_hooks -version: 0.18.2+1 +version: 0.18.3 environment: sdk: ">=2.12.0 <3.0.0" From b93b3b23cfe5e2143c37174e80dd6eceff6714a7 Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Mon, 18 Apr 2022 08:35:26 +0200 Subject: [PATCH 259/384] Update build.yml --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 7229edb4..ae9881db 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -17,7 +17,7 @@ jobs: - flutter_hooks channel: - dev - # - stable + - stable steps: - uses: actions/checkout@v2 From 11b8cd2b1d1cde63e27c39a0d35fc155c5c0c67a Mon Sep 17 00:00:00 2001 From: Ronnie <60412984+ronnieeeeee@users.noreply.github.com> Date: Mon, 18 Apr 2022 15:44:17 +0900 Subject: [PATCH 260/384] Fix dart embedded in README.md to null safety. (#298) --- README.md | 4 ++-- packages/flutter_hooks/lib/src/widgets_binding_observer.dart | 3 +++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index f81c344c..345ec89c 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ logic of say `initState` or `dispose`. An obvious example is `AnimationControlle class Example extends StatefulWidget { final Duration duration; - const Example({Key key, required this.duration}) + const Example({Key? key, required this.duration}) : super(key: key); @override @@ -74,7 +74,7 @@ This library proposes a third solution: ```dart class Example extends HookWidget { - const Example({Key key, required this.duration}) + const Example({Key? key, required this.duration}) : super(key: key); final Duration duration; diff --git a/packages/flutter_hooks/lib/src/widgets_binding_observer.dart b/packages/flutter_hooks/lib/src/widgets_binding_observer.dart index 70ec3d10..ce9009f8 100644 --- a/packages/flutter_hooks/lib/src/widgets_binding_observer.dart +++ b/packages/flutter_hooks/lib/src/widgets_binding_observer.dart @@ -39,7 +39,9 @@ class __AppLifecycleStateState @override void initHook() { super.initHook(); + // ignore: unnecessary_non_null_assertion _state = WidgetsBinding.instance!.lifecycleState; + // ignore: unnecessary_non_null_assertion WidgetsBinding.instance!.addObserver(this); } @@ -49,6 +51,7 @@ class __AppLifecycleStateState @override void dispose() { super.dispose(); + // ignore: unnecessary_non_null_assertion WidgetsBinding.instance!.removeObserver(this); } From 3c8a732672e0ed6f2e3d83b77292be025c507612 Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Mon, 18 Apr 2022 09:41:43 +0200 Subject: [PATCH 261/384] Add sponsors to readme --- README.md | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index f81c344c..272a5a33 100644 --- a/README.md +++ b/README.md @@ -294,7 +294,7 @@ Flutter_Hooks already comes with a list of reusable hooks which are divided into A set of low-level hooks that interact with the different life-cycles of a widget | Name | Description | -| ----------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------| +| ----------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------- | | [useEffect](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useEffect.html) | Useful for side-effects and optionally canceling them. | | [useState](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useState.html) | Creates a variable and subscribes to it. | | [useMemoized](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useMemoized.html) | Caches the instance of a complex object. | @@ -311,7 +311,7 @@ They will take care of creating/updating/disposing an object. #### dart:async related hooks: | Name | Description | -| ------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------| +| ------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------- | | [useStream](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useStream.html) | Subscribes to a `Stream` and returns its current state as an `AsyncSnapshot`. | | [useStreamController](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useStreamController.html) | Creates a `StreamController` which will automatically be disposed. | | [useFuture](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useFuture.html) | Subscribes to a `Future` and returns its current state as an `AsyncSnapshot`. | @@ -319,7 +319,7 @@ They will take care of creating/updating/disposing an object. #### Animation related hooks: | Name | Description | -| --------------------------------------------------------------------------------------------------------------------------------- | -----------------------------------------------------------------------| +| --------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------- | | [useSingleTickerProvider](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useSingleTickerProvider.html) | Creates a single usage `TickerProvider`. | | [useAnimationController](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useAnimationController.html) | Creates an `AnimationController` which will be automatically disposed. | | [useAnimation](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useAnimation.html) | Subscribes to an `Animation` and returns its value. | @@ -327,7 +327,7 @@ They will take care of creating/updating/disposing an object. #### Listenable related hooks: | Name | Description | -| ----------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------| +| ----------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------- | | [useListenable](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useListenable.html) | Subscribes to a `Listenable` and marks the widget as needing build whenever the listener is called. | | [useValueNotifier](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useValueNotifier.html) | Creates a `ValueNotifier` which will be automatically disposed. | | [useValueListenable](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useValueListenable.html) | Subscribes to a `ValueListenable` and return its value. | @@ -337,7 +337,7 @@ They will take care of creating/updating/disposing an object. A series of hooks with no particular theme. | Name | Description | -| ------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------| +| ------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------- | | [useReducer](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useReducer.html) | An alternative to `useState` for more complex states. | | [usePrevious](https://pub.dartlang.org/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`. | @@ -374,3 +374,11 @@ For a custom-hook to be merged, you will need to do the following: in the future. - Add it to the README and write documentation for it. + +## Sponsors + +

+ + + +

From cdad23e17131ccff4a84e3dcaa66b7f6b7447319 Mon Sep 17 00:00:00 2001 From: chunghha Date: Tue, 26 Apr 2022 12:42:47 -0500 Subject: [PATCH 262/384] chore: fix typos (#300) --- README.md | 4 ++-- packages/flutter_hooks/CHANGELOG.md | 6 ++--- packages/flutter_hooks/analysis_options.yaml | 2 +- .../flutter_hooks/lib/src/primitives.dart | 4 ++-- .../flutter_hooks/test/hook_widget_test.dart | 22 +++++++++---------- 5 files changed, 19 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index 727fe26d..9f7a7b2d 100644 --- a/README.md +++ b/README.md @@ -215,7 +215,7 @@ useC(); ``` In this situation, `HookA` maintains its state but `HookC` gets hard reset. -This happens because, when a hot-reload is perfomed after refactoring, all hooks _after_ the first line impacted are disposed of. +This happens because, when a hot-reload is performed after refactoring, all hooks _after_ the first line impacted are disposed of. So, since `HookC` was placed _after_ `HookB`, it will be disposed. ## How to create a hook @@ -370,7 +370,7 @@ For a custom-hook to be merged, you will need to do the following: - Write tests for your hook - A hook will not be merged unless fully tested to avoid inadvertendly breaking it + A hook will not be merged unless fully tested to avoid inadvertently breaking it in the future. - Add it to the README and write documentation for it. diff --git a/packages/flutter_hooks/CHANGELOG.md b/packages/flutter_hooks/CHANGELOG.md index 61b489b1..e6067672 100644 --- a/packages/flutter_hooks/CHANGELOG.md +++ b/packages/flutter_hooks/CHANGELOG.md @@ -62,7 +62,7 @@ Migrated flutter_hooks to null-safety (special thanks to @DevNico for the help!) ## 0.14.0 - added all `FocusNode` parameters to `useFocusNode` -- Fixed a bug where on hot-reload, a `HookWidget` could potentailly not rebuild +- Fixed a bug where on hot-reload, a `HookWidget` could potentially not rebuild - Allow hooks to integrate with the devtool using the `Diagnosticable` API, and implement it for all built-in hooks. @@ -137,7 +137,7 @@ Migrated flutter_hooks to null-safety (special thanks to @DevNico for the help!) ```dart // Creates an AnimationController final animationController = useAnimationController(); - // Immediatly listen to the AnimationController + // Immediately listen to the AnimationController useListenable(animationController); ``` @@ -187,7 +187,7 @@ Added `useFocusNode` - NEW: If a widget throws on the first build or after a hot-reload, next rebuilds can still add/edit hooks until one `build` finishes entirely. - NEW: new life-cycle available on `HookState`: `didBuild`. This life-cycle is called synchronously right after `build` method of `HookWidget` finished. -- NEW: new `reassemble` life-cycle on `HookState`. It is equivalent to `State.ressemble` of statefulwidgets. +- NEW: new `reassemble` life-cycle on `HookState`. It is equivalent to `State.reassemble` of statefulwidgets. - NEW: `useStream` and `useFuture` now have an optional `preserveState` flag. This toggle how these hooks behave when changing the stream/future: If true (default) they keep the previous value, else they reset to initialState. diff --git a/packages/flutter_hooks/analysis_options.yaml b/packages/flutter_hooks/analysis_options.yaml index 8c76de8f..386215f1 100644 --- a/packages/flutter_hooks/analysis_options.yaml +++ b/packages/flutter_hooks/analysis_options.yaml @@ -31,7 +31,7 @@ linter: always_specify_types: false # Incompatible with `prefer_final_locals` - # Having immutable local variables makes larger functions more predictible + # Having immutable local variables makes larger functions more predictable # so we will use `prefer_final_locals` instead. unnecessary_final: false diff --git a/packages/flutter_hooks/lib/src/primitives.dart b/packages/flutter_hooks/lib/src/primitives.dart index 905637af..9838526f 100644 --- a/packages/flutter_hooks/lib/src/primitives.dart +++ b/packages/flutter_hooks/lib/src/primitives.dart @@ -18,13 +18,13 @@ class ObjectRef { /// Creates an object that contains a single mutable property. /// /// Mutating the object's property has no effect. -/// This is useful for sharing state accross `build` calls, without causing +/// This is useful for sharing state across `build` calls, without causing /// unnecessary rebuilds. ObjectRef useRef(T initialValue) { return useMemoized(() => ObjectRef(initialValue)); } -/// Cache a function accross rebuilds based on a list of keys. +/// Cache a function across rebuilds based on a list of keys. /// /// This is syntax sugar for [useMemoized], so that instead of: /// diff --git a/packages/flutter_hooks/test/hook_widget_test.dart b/packages/flutter_hooks/test/hook_widget_test.dart index 5a9327cd..15b7a5be 100644 --- a/packages/flutter_hooks/test/hook_widget_test.dart +++ b/packages/flutter_hooks/test/hook_widget_test.dart @@ -38,7 +38,7 @@ void main() { ); } - void verifyNoMoreHookInteration() { + void verifyNoMoreHookInteraction() { verifyNoMoreInteractions(build); verifyNoMoreInteractions(dispose); verifyNoMoreInteractions(initHook); @@ -503,7 +503,7 @@ void main() { initHook(), build(context), ]); - verifyNoMoreHookInteration(); + verifyNoMoreHookInteraction(); await tester.pumpWidget($build()); @@ -511,7 +511,7 @@ void main() { didUpdateHook(any), build(context), ]); - verifyNoMoreHookInteration(); + verifyNoMoreHookInteraction(); // from null to array keys = []; @@ -523,7 +523,7 @@ void main() { build(context), dispose(), ]); - verifyNoMoreHookInteration(); + verifyNoMoreHookInteraction(); // array immutable keys.add(42); @@ -534,7 +534,7 @@ void main() { didUpdateHook(any), build(context), ]); - verifyNoMoreHookInteration(); + verifyNoMoreHookInteraction(); // new array but content equal keys = [42]; @@ -545,7 +545,7 @@ void main() { didUpdateHook(any), build(context), ]); - verifyNoMoreHookInteration(); + verifyNoMoreHookInteraction(); // new array new content keys = [44]; @@ -558,7 +558,7 @@ void main() { build(context), dispose(), ]); - verifyNoMoreHookInteration(); + verifyNoMoreHookInteraction(); }); testWidgets('hook & setState', (tester) async { @@ -605,7 +605,7 @@ void main() { initHook(), build(any), ]); - verifyNoMoreHookInteration(); + verifyNoMoreHookInteraction(); when(build(context)).thenReturn(24); var previousHook = hook; @@ -623,17 +623,17 @@ void main() { didUpdateHook(previousHook), build(context), ]); - verifyNoMoreHookInteration(); + verifyNoMoreHookInteraction(); previousHook = hook; await tester.pump(); - verifyNoMoreHookInteration(); + verifyNoMoreHookInteraction(); await tester.pumpWidget(const SizedBox()); verify(dispose()).called(1); - verifyNoMoreHookInteration(); + verifyNoMoreHookInteraction(); }); testWidgets('dispose all called even on failed', (tester) async { From fc4968ed22439f2c56fee63966b48a4a53528cab Mon Sep 17 00:00:00 2001 From: Daichan Date: Thu, 12 May 2022 05:29:13 -0400 Subject: [PATCH 263/384] bug fix flutter 3.0 breaking changes (#304) fixes #303 --- .../flutter_hooks/lib/src/widgets_binding_observer.dart | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/packages/flutter_hooks/lib/src/widgets_binding_observer.dart b/packages/flutter_hooks/lib/src/widgets_binding_observer.dart index ce9009f8..2ff9bb0d 100644 --- a/packages/flutter_hooks/lib/src/widgets_binding_observer.dart +++ b/packages/flutter_hooks/lib/src/widgets_binding_observer.dart @@ -39,10 +39,8 @@ class __AppLifecycleStateState @override void initHook() { super.initHook(); - // ignore: unnecessary_non_null_assertion - _state = WidgetsBinding.instance!.lifecycleState; - // ignore: unnecessary_non_null_assertion - WidgetsBinding.instance!.addObserver(this); + _state = WidgetsBinding.instance.lifecycleState; + WidgetsBinding.instance.addObserver(this); } @override @@ -51,8 +49,7 @@ class __AppLifecycleStateState @override void dispose() { super.dispose(); - // ignore: unnecessary_non_null_assertion - WidgetsBinding.instance!.removeObserver(this); + WidgetsBinding.instance.removeObserver(this); } @override From 76f616510a13d8e32e0e781fa26826f890ee2bbb Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Thu, 12 May 2022 11:30:14 +0200 Subject: [PATCH 264/384] 0.18.4 --- packages/flutter_hooks/CHANGELOG.md | 4 ++++ packages/flutter_hooks/pubspec.yaml | 6 +++--- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/flutter_hooks/CHANGELOG.md b/packages/flutter_hooks/CHANGELOG.md index e6067672..ac53ecbc 100644 --- a/packages/flutter_hooks/CHANGELOG.md +++ b/packages/flutter_hooks/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.18.4 + +Upgrade to Flutter 3.0.0 + ## 0.18.3 Added `onKeyEvent` to `useFocusNode` (thanks to @kdelorey) diff --git a/packages/flutter_hooks/pubspec.yaml b/packages/flutter_hooks/pubspec.yaml index 8912db2d..515f7062 100644 --- a/packages/flutter_hooks/pubspec.yaml +++ b/packages/flutter_hooks/pubspec.yaml @@ -1,11 +1,11 @@ name: flutter_hooks description: A flutter implementation of React hooks. It adds a new kind of widget with enhanced code reuse. homepage: https://github.com/rrousselGit/flutter_hooks -version: 0.18.3 +version: 0.18.4 environment: - sdk: ">=2.12.0 <3.0.0" - flutter: ">=1.20.0" + sdk: ">=2.17.0 <3.0.0" + flutter: ">=3.0.0" dependencies: flutter: From fd649f8a4cab45ad48d38cc8c1e20c05452009b6 Mon Sep 17 00:00:00 2001 From: lask Date: Tue, 17 May 2022 06:59:11 -0300 Subject: [PATCH 265/384] Update README.md (#305) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 9f7a7b2d..a470d2e5 100644 --- a/README.md +++ b/README.md @@ -341,7 +341,7 @@ A series of hooks with no particular theme. | [useReducer](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useReducer.html) | An alternative to `useState` for more complex states. | | [usePrevious](https://pub.dartlang.org/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.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useFocusNode.html) | Createx a `FocusNode`. | +| [useFocusNode](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useFocusNode.html) | Creates a `FocusNode`. | | [useTabController](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useTabController.html) | Creates and disposes a `TabController`. | | [useScrollController](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useScrollController.html) | Creates and disposes a `ScrollController`. | | [usePageController](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/usePageController.html) | Creates and disposes a `PageController`. | From fa1c646c64231353ec6e208183a95613109bd031 Mon Sep 17 00:00:00 2001 From: ipcjs Date: Fri, 17 Jun 2022 22:28:51 +0800 Subject: [PATCH 266/384] Fix the compile error of example (#306) * Fix the compile error of example. * ignore files * Use explicit version constraints Co-authored-by: Remi Rousselet --- packages/flutter_hooks/.gitignore | 3 +- packages/flutter_hooks/example/.gitignore | 3 + .../example/lib/star_wars/models.g.dart | 73 ++- .../example/lib/star_wars/redux.g.dart | 38 +- .../example/lib/star_wars/star_wars_api.dart | 2 +- packages/flutter_hooks/example/pubspec.lock | 600 ++++++++++++++++++ packages/flutter_hooks/example/pubspec.yaml | 11 +- 7 files changed, 670 insertions(+), 60 deletions(-) create mode 100644 packages/flutter_hooks/example/pubspec.lock diff --git a/packages/flutter_hooks/.gitignore b/packages/flutter_hooks/.gitignore index 9a2e2c11..d85365b2 100644 --- a/packages/flutter_hooks/.gitignore +++ b/packages/flutter_hooks/.gitignore @@ -11,4 +11,5 @@ ios/ /coverage pubspec.lock .vscode/ -.fvm \ No newline at end of file +.fvm +.idea/ diff --git a/packages/flutter_hooks/example/.gitignore b/packages/flutter_hooks/example/.gitignore index 83ebb9e9..f1c88358 100644 --- a/packages/flutter_hooks/example/.gitignore +++ b/packages/flutter_hooks/example/.gitignore @@ -70,3 +70,6 @@ build/ !**/ios/**/default.pbxuser !**/ios/**/default.perspectivev3 !/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages + +generated_plugin_registrant.dart +/test/ diff --git a/packages/flutter_hooks/example/lib/star_wars/models.g.dart b/packages/flutter_hooks/example/lib/star_wars/models.g.dart index fdd9262f..dd7ab833 100644 --- a/packages/flutter_hooks/example/lib/star_wars/models.g.dart +++ b/packages/flutter_hooks/example/lib/star_wars/models.g.dart @@ -33,16 +33,19 @@ class _$PlanetPageModelSerializer specifiedType: const FullType(BuiltList, const [const FullType(PlanetModel)])), ]; - if (object.next != null) { + Object value; + value = object.next; + if (value != null) { result ..add('next') - ..add(serializers.serialize(object.next, + ..add(serializers.serialize(value, specifiedType: const FullType(String))); } - if (object.previous != null) { + value = object.previous; + if (value != null) { result ..add('previous') - ..add(serializers.serialize(object.previous, + ..add(serializers.serialize(value, specifiedType: const FullType(String))); } return result; @@ -58,7 +61,7 @@ class _$PlanetPageModelSerializer while (iterator.moveNext()) { final key = iterator.current as String; iterator.moveNext(); - final dynamic value = iterator.current; + final Object value = iterator.current; switch (key) { case 'next': result.next = serializers.deserialize(value, @@ -72,7 +75,7 @@ class _$PlanetPageModelSerializer result.results.replace(serializers.deserialize(value, specifiedType: const FullType( BuiltList, const [const FullType(PlanetModel)])) - as BuiltList); + as BuiltList); break; } } @@ -107,7 +110,7 @@ class _$PlanetModelSerializer implements StructuredSerializer { while (iterator.moveNext()) { final key = iterator.current as String; iterator.moveNext(); - final dynamic value = iterator.current; + final Object value = iterator.current; switch (key) { case 'name': result.name = serializers.deserialize(value, @@ -129,12 +132,11 @@ class _$PlanetPageModel extends PlanetPageModel { final BuiltList results; factory _$PlanetPageModel([void Function(PlanetPageModelBuilder) updates]) => - (new PlanetPageModelBuilder()..update(updates)).build(); + (new PlanetPageModelBuilder()..update(updates))._build(); _$PlanetPageModel._({this.next, this.previous, this.results}) : super._() { - if (results == null) { - throw new BuiltValueNullFieldError('PlanetPageModel', 'results'); - } + BuiltValueNullFieldError.checkNotNull( + results, r'PlanetPageModel', 'results'); } @override @@ -162,7 +164,7 @@ class _$PlanetPageModel extends PlanetPageModel { @override String toString() { - return (newBuiltValueToStringHelper('PlanetPageModel') + return (newBuiltValueToStringHelper(r'PlanetPageModel') ..add('next', next) ..add('previous', previous) ..add('results', results)) @@ -190,10 +192,11 @@ class PlanetPageModelBuilder PlanetPageModelBuilder(); PlanetPageModelBuilder get _$this { - if (_$v != null) { - _next = _$v.next; - _previous = _$v.previous; - _results = _$v.results?.toBuilder(); + final $v = _$v; + if ($v != null) { + _next = $v.next; + _previous = $v.previous; + _results = $v.results.toBuilder(); _$v = null; } return this; @@ -201,9 +204,7 @@ class PlanetPageModelBuilder @override void replace(PlanetPageModel other) { - if (other == null) { - throw new ArgumentError.notNull('other'); - } + ArgumentError.checkNotNull(other, 'other'); _$v = other as _$PlanetPageModel; } @@ -213,7 +214,9 @@ class PlanetPageModelBuilder } @override - _$PlanetPageModel build() { + PlanetPageModel build() => _build(); + + _$PlanetPageModel _build() { _$PlanetPageModel _$result; try { _$result = _$v ?? @@ -226,7 +229,7 @@ class PlanetPageModelBuilder results.build(); } catch (e) { throw new BuiltValueNestedFieldError( - 'PlanetPageModel', _$failedField, e.toString()); + r'PlanetPageModel', _$failedField, e.toString()); } rethrow; } @@ -240,12 +243,10 @@ class _$PlanetModel extends PlanetModel { final String name; factory _$PlanetModel([void Function(PlanetModelBuilder) updates]) => - (new PlanetModelBuilder()..update(updates)).build(); + (new PlanetModelBuilder()..update(updates))._build(); _$PlanetModel._({this.name}) : super._() { - if (name == null) { - throw new BuiltValueNullFieldError('PlanetModel', 'name'); - } + BuiltValueNullFieldError.checkNotNull(name, r'PlanetModel', 'name'); } @override @@ -268,7 +269,7 @@ class _$PlanetModel extends PlanetModel { @override String toString() { - return (newBuiltValueToStringHelper('PlanetModel')..add('name', name)) + return (newBuiltValueToStringHelper(r'PlanetModel')..add('name', name)) .toString(); } } @@ -283,8 +284,9 @@ class PlanetModelBuilder implements Builder { PlanetModelBuilder(); PlanetModelBuilder get _$this { - if (_$v != null) { - _name = _$v.name; + final $v = _$v; + if ($v != null) { + _name = $v.name; _$v = null; } return this; @@ -292,9 +294,7 @@ class PlanetModelBuilder implements Builder { @override void replace(PlanetModel other) { - if (other == null) { - throw new ArgumentError.notNull('other'); - } + ArgumentError.checkNotNull(other, 'other'); _$v = other as _$PlanetModel; } @@ -304,11 +304,16 @@ class PlanetModelBuilder implements Builder { } @override - _$PlanetModel build() { - final _$result = _$v ?? new _$PlanetModel._(name: name); + PlanetModel build() => _build(); + + _$PlanetModel _build() { + final _$result = _$v ?? + new _$PlanetModel._( + name: BuiltValueNullFieldError.checkNotNull( + name, r'PlanetModel', 'name')); replace(_$result); return _$result; } } -// ignore_for_file: always_put_control_body_on_new_line,always_specify_types,annotate_overrides,avoid_annotating_with_dynamic,avoid_as,avoid_catches_without_on_clauses,avoid_returning_this,lines_longer_than_80_chars,omit_local_variable_types,prefer_expression_function_bodies,sort_constructors_first,test_types_in_equals,unnecessary_const,unnecessary_new +// ignore_for_file: always_put_control_body_on_new_line,always_specify_types,annotate_overrides,avoid_annotating_with_dynamic,avoid_as,avoid_catches_without_on_clauses,avoid_returning_this,deprecated_member_use_from_same_package,lines_longer_than_80_chars,no_leading_underscores_for_local_identifiers,omit_local_variable_types,prefer_expression_function_bodies,sort_constructors_first,test_types_in_equals,unnecessary_const,unnecessary_new,unnecessary_lambdas diff --git a/packages/flutter_hooks/example/lib/star_wars/redux.g.dart b/packages/flutter_hooks/example/lib/star_wars/redux.g.dart index c7e04e0f..df832749 100644 --- a/packages/flutter_hooks/example/lib/star_wars/redux.g.dart +++ b/packages/flutter_hooks/example/lib/star_wars/redux.g.dart @@ -15,17 +15,15 @@ class _$AppState extends AppState { final PlanetPageModel planetPage; factory _$AppState([void Function(AppStateBuilder) updates]) => - (new AppStateBuilder()..update(updates)).build(); + (new AppStateBuilder()..update(updates))._build(); _$AppState._( {this.isFetchingPlanets, this.errorFetchingPlanets, this.planetPage}) : super._() { - if (isFetchingPlanets == null) { - throw new BuiltValueNullFieldError('AppState', 'isFetchingPlanets'); - } - if (planetPage == null) { - throw new BuiltValueNullFieldError('AppState', 'planetPage'); - } + BuiltValueNullFieldError.checkNotNull( + isFetchingPlanets, r'AppState', 'isFetchingPlanets'); + BuiltValueNullFieldError.checkNotNull( + planetPage, r'AppState', 'planetPage'); } @override @@ -53,7 +51,7 @@ class _$AppState extends AppState { @override String toString() { - return (newBuiltValueToStringHelper('AppState') + return (newBuiltValueToStringHelper(r'AppState') ..add('isFetchingPlanets', isFetchingPlanets) ..add('errorFetchingPlanets', errorFetchingPlanets) ..add('planetPage', planetPage)) @@ -83,10 +81,11 @@ class AppStateBuilder implements Builder { AppStateBuilder(); AppStateBuilder get _$this { - if (_$v != null) { - _isFetchingPlanets = _$v.isFetchingPlanets; - _errorFetchingPlanets = _$v.errorFetchingPlanets; - _planetPage = _$v.planetPage?.toBuilder(); + final $v = _$v; + if ($v != null) { + _isFetchingPlanets = $v.isFetchingPlanets; + _errorFetchingPlanets = $v.errorFetchingPlanets; + _planetPage = $v.planetPage.toBuilder(); _$v = null; } return this; @@ -94,9 +93,7 @@ class AppStateBuilder implements Builder { @override void replace(AppState other) { - if (other == null) { - throw new ArgumentError.notNull('other'); - } + ArgumentError.checkNotNull(other, 'other'); _$v = other as _$AppState; } @@ -106,12 +103,15 @@ class AppStateBuilder implements Builder { } @override - _$AppState build() { + AppState build() => _build(); + + _$AppState _build() { _$AppState _$result; try { _$result = _$v ?? new _$AppState._( - isFetchingPlanets: isFetchingPlanets, + isFetchingPlanets: BuiltValueNullFieldError.checkNotNull( + isFetchingPlanets, r'AppState', 'isFetchingPlanets'), errorFetchingPlanets: errorFetchingPlanets, planetPage: planetPage.build()); } catch (_) { @@ -121,7 +121,7 @@ class AppStateBuilder implements Builder { planetPage.build(); } catch (e) { throw new BuiltValueNestedFieldError( - 'AppState', _$failedField, e.toString()); + r'AppState', _$failedField, e.toString()); } rethrow; } @@ -130,4 +130,4 @@ class AppStateBuilder implements Builder { } } -// ignore_for_file: always_put_control_body_on_new_line,always_specify_types,annotate_overrides,avoid_annotating_with_dynamic,avoid_as,avoid_catches_without_on_clauses,avoid_returning_this,lines_longer_than_80_chars,omit_local_variable_types,prefer_expression_function_bodies,sort_constructors_first,test_types_in_equals,unnecessary_const,unnecessary_new +// ignore_for_file: always_put_control_body_on_new_line,always_specify_types,annotate_overrides,avoid_annotating_with_dynamic,avoid_as,avoid_catches_without_on_clauses,avoid_returning_this,deprecated_member_use_from_same_package,lines_longer_than_80_chars,no_leading_underscores_for_local_identifiers,omit_local_variable_types,prefer_expression_function_bodies,sort_constructors_first,test_types_in_equals,unnecessary_const,unnecessary_new,unnecessary_lambdas diff --git a/packages/flutter_hooks/example/lib/star_wars/star_wars_api.dart b/packages/flutter_hooks/example/lib/star_wars/star_wars_api.dart index ed48693d..9f0f7286 100644 --- a/packages/flutter_hooks/example/lib/star_wars/star_wars_api.dart +++ b/packages/flutter_hooks/example/lib/star_wars/star_wars_api.dart @@ -10,7 +10,7 @@ class StarWarsApi { Future getPlanets([String page]) async { page ??= 'https://swapi.dev/api/planets'; - final response = await http.get(page); + final response = await http.get(Uri.parse(page)); final dynamic json = jsonDecode(utf8.decode(response.bodyBytes)); return serializers.deserializeWith(PlanetPageModel.serializer, json); diff --git a/packages/flutter_hooks/example/pubspec.lock b/packages/flutter_hooks/example/pubspec.lock new file mode 100644 index 00000000..9315675d --- /dev/null +++ b/packages/flutter_hooks/example/pubspec.lock @@ -0,0 +1,600 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + _fe_analyzer_shared: + dependency: transitive + description: + name: _fe_analyzer_shared + url: "https://pub.dartlang.org" + source: hosted + version: "40.0.0" + analyzer: + dependency: transitive + description: + name: analyzer + url: "https://pub.dartlang.org" + source: hosted + version: "4.1.0" + args: + dependency: transitive + description: + name: args + url: "https://pub.dartlang.org" + source: hosted + version: "2.3.1" + async: + dependency: transitive + description: + name: async + url: "https://pub.dartlang.org" + source: hosted + version: "2.8.2" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.0" + build: + dependency: transitive + description: + name: build + url: "https://pub.dartlang.org" + source: hosted + version: "2.3.0" + build_config: + dependency: transitive + description: + name: build_config + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.0" + build_daemon: + dependency: transitive + description: + name: build_daemon + url: "https://pub.dartlang.org" + source: hosted + version: "3.1.0" + build_resolvers: + dependency: transitive + description: + name: build_resolvers + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.8" + build_runner: + dependency: "direct dev" + description: + name: build_runner + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.11" + build_runner_core: + dependency: transitive + description: + name: build_runner_core + url: "https://pub.dartlang.org" + source: hosted + version: "7.2.3" + built_collection: + dependency: "direct main" + description: + name: built_collection + url: "https://pub.dartlang.org" + source: hosted + version: "5.1.1" + built_value: + dependency: "direct main" + description: + name: built_value + url: "https://pub.dartlang.org" + source: hosted + version: "8.3.0" + built_value_generator: + dependency: "direct dev" + description: + name: built_value_generator + url: "https://pub.dartlang.org" + source: hosted + version: "8.3.3" + characters: + dependency: transitive + description: + name: characters + url: "https://pub.dartlang.org" + source: hosted + version: "1.2.0" + charcode: + dependency: transitive + description: + name: charcode + url: "https://pub.dartlang.org" + source: hosted + version: "1.3.1" + checked_yaml: + dependency: transitive + description: + name: checked_yaml + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.1" + clock: + dependency: transitive + description: + name: clock + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.0" + code_builder: + dependency: transitive + description: + name: code_builder + url: "https://pub.dartlang.org" + source: hosted + version: "4.1.0" + collection: + dependency: transitive + description: + name: collection + url: "https://pub.dartlang.org" + source: hosted + version: "1.16.0" + convert: + dependency: transitive + description: + name: convert + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.1" + crypto: + dependency: transitive + description: + name: crypto + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.2" + dart_style: + dependency: transitive + description: + name: dart_style + url: "https://pub.dartlang.org" + source: hosted + version: "2.2.3" + fake_async: + dependency: transitive + description: + name: fake_async + url: "https://pub.dartlang.org" + source: hosted + version: "1.3.0" + ffi: + dependency: transitive + description: + name: ffi + url: "https://pub.dartlang.org" + source: hosted + version: "1.2.1" + file: + dependency: transitive + description: + name: file + url: "https://pub.dartlang.org" + source: hosted + version: "6.1.2" + fixnum: + dependency: transitive + description: + name: fixnum + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.1" + flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_hooks: + dependency: "direct main" + description: + path: ".." + relative: true + source: path + version: "0.18.4" + flutter_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + flutter_web_plugins: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + frontend_server_client: + dependency: transitive + description: + name: frontend_server_client + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.3" + glob: + dependency: transitive + description: + name: glob + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.2" + graphs: + dependency: transitive + description: + name: graphs + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.0" + http: + dependency: "direct main" + description: + name: http + url: "https://pub.dartlang.org" + source: hosted + version: "0.13.4" + http_multi_server: + dependency: transitive + description: + name: http_multi_server + url: "https://pub.dartlang.org" + source: hosted + version: "3.2.0" + http_parser: + dependency: transitive + description: + name: http_parser + url: "https://pub.dartlang.org" + source: hosted + version: "4.0.1" + io: + dependency: transitive + description: + name: io + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.3" + js: + dependency: transitive + description: + name: js + url: "https://pub.dartlang.org" + source: hosted + version: "0.6.4" + json_annotation: + dependency: transitive + description: + name: json_annotation + url: "https://pub.dartlang.org" + source: hosted + version: "4.5.0" + logging: + dependency: transitive + description: + name: logging + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.2" + matcher: + dependency: transitive + description: + name: matcher + url: "https://pub.dartlang.org" + source: hosted + version: "0.12.11" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.4" + meta: + dependency: transitive + description: + name: meta + url: "https://pub.dartlang.org" + source: hosted + version: "1.7.0" + mime: + dependency: transitive + description: + name: mime + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.2" + nested: + dependency: transitive + description: + name: nested + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.0" + package_config: + dependency: transitive + description: + name: package_config + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.2" + path: + dependency: transitive + description: + name: path + url: "https://pub.dartlang.org" + source: hosted + version: "1.8.1" + path_provider_linux: + dependency: transitive + description: + name: path_provider_linux + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.6" + path_provider_platform_interface: + dependency: transitive + description: + name: path_provider_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.4" + path_provider_windows: + dependency: transitive + description: + name: path_provider_windows + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.6" + platform: + dependency: transitive + description: + name: platform + url: "https://pub.dartlang.org" + source: hosted + version: "3.1.0" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.2" + pool: + dependency: transitive + description: + name: pool + url: "https://pub.dartlang.org" + source: hosted + version: "1.5.0" + process: + dependency: transitive + description: + name: process + url: "https://pub.dartlang.org" + source: hosted + version: "4.2.4" + provider: + dependency: "direct main" + description: + name: provider + url: "https://pub.dartlang.org" + source: hosted + version: "4.3.3" + pub_semver: + dependency: transitive + description: + name: pub_semver + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.1" + pubspec_parse: + dependency: transitive + description: + name: pubspec_parse + url: "https://pub.dartlang.org" + source: hosted + version: "1.2.0" + quiver: + dependency: transitive + description: + name: quiver + url: "https://pub.dartlang.org" + source: hosted + version: "3.1.0" + shared_preferences: + dependency: "direct main" + description: + name: shared_preferences + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.15" + shared_preferences_android: + dependency: transitive + description: + name: shared_preferences_android + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.12" + shared_preferences_ios: + dependency: transitive + description: + name: shared_preferences_ios + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.1" + shared_preferences_linux: + dependency: transitive + description: + name: shared_preferences_linux + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.1" + shared_preferences_macos: + dependency: transitive + description: + name: shared_preferences_macos + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.4" + shared_preferences_platform_interface: + dependency: transitive + description: + name: shared_preferences_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.0" + shared_preferences_web: + dependency: transitive + description: + name: shared_preferences_web + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.4" + shared_preferences_windows: + dependency: transitive + description: + name: shared_preferences_windows + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.1" + shelf: + dependency: transitive + description: + name: shelf + url: "https://pub.dartlang.org" + source: hosted + version: "1.3.0" + shelf_web_socket: + dependency: transitive + description: + name: shelf_web_socket + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.1" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.99" + source_gen: + dependency: transitive + description: + name: source_gen + url: "https://pub.dartlang.org" + source: hosted + version: "1.2.2" + source_span: + dependency: transitive + description: + name: source_span + url: "https://pub.dartlang.org" + source: hosted + version: "1.8.2" + stack_trace: + dependency: transitive + description: + name: stack_trace + url: "https://pub.dartlang.org" + source: hosted + version: "1.10.0" + stream_channel: + dependency: transitive + description: + name: stream_channel + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.0" + stream_transform: + dependency: transitive + description: + name: stream_transform + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.0" + string_scanner: + dependency: transitive + description: + name: string_scanner + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.0" + term_glyph: + dependency: transitive + description: + name: term_glyph + url: "https://pub.dartlang.org" + source: hosted + version: "1.2.0" + test_api: + dependency: transitive + description: + name: test_api + url: "https://pub.dartlang.org" + source: hosted + version: "0.4.9" + timing: + dependency: transitive + description: + name: timing + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.0" + typed_data: + dependency: transitive + description: + name: typed_data + url: "https://pub.dartlang.org" + source: hosted + version: "1.3.1" + vector_math: + dependency: transitive + description: + name: vector_math + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.2" + watcher: + dependency: transitive + description: + name: watcher + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.1" + web_socket_channel: + dependency: transitive + description: + name: web_socket_channel + url: "https://pub.dartlang.org" + source: hosted + version: "2.2.0" + win32: + dependency: transitive + description: + name: win32 + url: "https://pub.dartlang.org" + source: hosted + version: "2.6.1" + xdg_directories: + dependency: transitive + description: + name: xdg_directories + url: "https://pub.dartlang.org" + source: hosted + version: "0.2.0+1" + yaml: + dependency: transitive + description: + name: yaml + url: "https://pub.dartlang.org" + source: hosted + version: "3.1.1" +sdks: + dart: ">=2.17.0 <3.0.0" + flutter: ">=3.0.0" diff --git a/packages/flutter_hooks/example/pubspec.yaml b/packages/flutter_hooks/example/pubspec.yaml index 9f8d0faf..f0001cf4 100644 --- a/packages/flutter_hooks/example/pubspec.yaml +++ b/packages/flutter_hooks/example/pubspec.yaml @@ -8,18 +8,19 @@ environment: sdk: ">=2.7.0 <3.0.0" dependencies: - built_collection: ">=2.0.0 <5.0.0" - built_value: ^6.0.0 + built_collection: ^5.0.0 + built_value: ^8.0.0 flutter: sdk: flutter flutter_hooks: path: ../ + http: ^0.13.4 provider: ^4.2.0 - shared_preferences: ^0.4.3 + shared_preferences: ^2.0.0 dev_dependencies: - build_runner: ^1.0.0 - built_value_generator: ^6.0.0 + build_runner: ^2.1.11 + built_value_generator: ^8.3.3 flutter_test: sdk: flutter From 157cc7f82735dc68e3b985ec2269e784632062f6 Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Fri, 17 Jun 2022 16:31:49 +0200 Subject: [PATCH 267/384] Disable dev branch --- .github/workflows/build.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ae9881db..3dd35503 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -16,7 +16,6 @@ jobs: package: - flutter_hooks channel: - - dev - stable steps: From f9cb0978a54eab884ec8e543e979855a32de99cf Mon Sep 17 00:00:00 2001 From: wheeOs <101183252+wheeOs@users.noreply.github.com> Date: Fri, 17 Jun 2022 17:04:35 +0200 Subject: [PATCH 268/384] Fix null-safety warning --- packages/flutter_hooks/example/pubspec.lock | 28 ++++++++++----------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/packages/flutter_hooks/example/pubspec.lock b/packages/flutter_hooks/example/pubspec.lock index 9315675d..8a0495a6 100644 --- a/packages/flutter_hooks/example/pubspec.lock +++ b/packages/flutter_hooks/example/pubspec.lock @@ -63,7 +63,7 @@ packages: name: build_resolvers url: "https://pub.dartlang.org" source: hosted - version: "2.0.8" + version: "2.0.9" build_runner: dependency: "direct dev" description: @@ -91,7 +91,7 @@ packages: name: built_value url: "https://pub.dartlang.org" source: hosted - version: "8.3.0" + version: "8.3.3" built_value_generator: dependency: "direct dev" description: @@ -147,7 +147,7 @@ packages: name: convert url: "https://pub.dartlang.org" source: hosted - version: "3.0.1" + version: "3.0.2" crypto: dependency: transitive description: @@ -175,7 +175,7 @@ packages: name: ffi url: "https://pub.dartlang.org" source: hosted - version: "1.2.1" + version: "2.0.0" file: dependency: transitive description: @@ -225,7 +225,7 @@ packages: name: glob url: "https://pub.dartlang.org" source: hosted - version: "2.0.2" + version: "2.1.0" graphs: dependency: transitive description: @@ -246,7 +246,7 @@ packages: name: http_multi_server url: "https://pub.dartlang.org" source: hosted - version: "3.2.0" + version: "3.2.1" http_parser: dependency: transitive description: @@ -323,7 +323,7 @@ packages: name: package_config url: "https://pub.dartlang.org" source: hosted - version: "2.0.2" + version: "2.1.0" path: dependency: transitive description: @@ -337,7 +337,7 @@ packages: name: path_provider_linux url: "https://pub.dartlang.org" source: hosted - version: "2.1.6" + version: "2.1.7" path_provider_platform_interface: dependency: transitive description: @@ -351,7 +351,7 @@ packages: name: path_provider_windows url: "https://pub.dartlang.org" source: hosted - version: "2.0.6" + version: "2.1.0" platform: dependency: transitive description: @@ -372,7 +372,7 @@ packages: name: pool url: "https://pub.dartlang.org" source: hosted - version: "1.5.0" + version: "1.5.1" process: dependency: transitive description: @@ -407,7 +407,7 @@ packages: name: quiver url: "https://pub.dartlang.org" source: hosted - version: "3.1.0" + version: "2.1.5" shared_preferences: dependency: "direct main" description: @@ -470,14 +470,14 @@ packages: name: shelf url: "https://pub.dartlang.org" source: hosted - version: "1.3.0" + version: "1.3.1" shelf_web_socket: dependency: transitive description: name: shelf_web_socket url: "https://pub.dartlang.org" source: hosted - version: "1.0.1" + version: "1.0.2" sky_engine: dependency: transitive description: flutter @@ -580,7 +580,7 @@ packages: name: win32 url: "https://pub.dartlang.org" source: hosted - version: "2.6.1" + version: "2.7.0" xdg_directories: dependency: transitive description: From 8ddd2418c7cd30ec1486b1ff707ef53a71a3fdfe Mon Sep 17 00:00:00 2001 From: Ronnie <60412984+ronnieeeeee@users.noreply.github.com> Date: Sat, 18 Jun 2022 00:33:01 +0900 Subject: [PATCH 269/384] add useListenableSelector (#308) --- README.md | 11 +- packages/flutter_hooks/CHANGELOG.md | 5 + packages/flutter_hooks/lib/src/hooks.dart | 1 + .../lib/src/listenable_selector.dart | 94 ++++++++++ .../test/use_listenable_selector_test.dart | 173 ++++++++++++++++++ 5 files changed, 279 insertions(+), 5 deletions(-) create mode 100644 packages/flutter_hooks/lib/src/listenable_selector.dart create mode 100644 packages/flutter_hooks/test/use_listenable_selector_test.dart diff --git a/README.md b/README.md index a470d2e5..a97554fe 100644 --- a/README.md +++ b/README.md @@ -326,11 +326,12 @@ They will take care of creating/updating/disposing an object. #### Listenable related hooks: -| Name | Description | -| ----------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------- | -| [useListenable](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useListenable.html) | Subscribes to a `Listenable` and marks the widget as needing build whenever the listener is called. | -| [useValueNotifier](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useValueNotifier.html) | Creates a `ValueNotifier` which will be automatically disposed. | -| [useValueListenable](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useValueListenable.html) | Subscribes to a `ValueListenable` and return its value. | +| Name | Description | +| ----------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------- | +| [useListenable](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useListenable.html) | Subscribes to a `Listenable` and marks the widget as needing build whenever the listener is called. | +| [useListenableSelector](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useListenableSelector.html) | Similar to `useListenable`, but allows filtering UI rebuilds | +| [useValueNotifier](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useValueNotifier.html) | Creates a `ValueNotifier` which will be automatically disposed. | +| [useValueListenable](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useValueListenable.html) | Subscribes to a `ValueListenable` and return its value. | #### Misc hooks: diff --git a/packages/flutter_hooks/CHANGELOG.md b/packages/flutter_hooks/CHANGELOG.md index ac53ecbc..4628d56a 100644 --- a/packages/flutter_hooks/CHANGELOG.md +++ b/packages/flutter_hooks/CHANGELOG.md @@ -1,3 +1,8 @@ +## Unreleased minor + +Added `useListenableSelector`, similar to `useListenable`, for listening to a +`Listenable` but rebuilding the widget only if a certain data has changed (thanks to @ronnieeeeee) + ## 0.18.4 Upgrade to Flutter 3.0.0 diff --git a/packages/flutter_hooks/lib/src/hooks.dart b/packages/flutter_hooks/lib/src/hooks.dart index 889a03ea..a102f8e4 100644 --- a/packages/flutter_hooks/lib/src/hooks.dart +++ b/packages/flutter_hooks/lib/src/hooks.dart @@ -19,3 +19,4 @@ part 'scroll_controller.dart'; part 'page_controller.dart'; part 'widgets_binding_observer.dart'; part 'transformation_controller.dart'; +part 'listenable_selector.dart'; diff --git a/packages/flutter_hooks/lib/src/listenable_selector.dart b/packages/flutter_hooks/lib/src/listenable_selector.dart new file mode 100644 index 00000000..c73becc4 --- /dev/null +++ b/packages/flutter_hooks/lib/src/listenable_selector.dart @@ -0,0 +1,94 @@ +part of 'hooks.dart'; + +/// Rebuild only when there is a change in the selector result. +/// +/// The following example showcases If no text is entered, you will not be able to press the button. +/// ```dart +/// class Example extends HookWidget { +/// @override +/// Widget build(BuildContext context) { +/// final listenable = useTextEditingController(); +/// final bool textIsEmpty = +/// useListenableSelector(listenable, () => listenable.text.isEmpty); +/// return Column( +/// children: [ +/// TextField(controller: listenable), +/// ElevatedButton( +/// // If no text is entered, the button cannot be pressed +/// onPressed: textIsEmpty ? null : () => print("Button can be pressed!"), +/// child: Text("Button")), +/// ], +/// ); +/// } +/// } +/// ``` +/// + +R useListenableSelector( + Listenable listenable, + R Function() selector, +) { + return use(_ListenableSelectorHook(listenable, selector)); +} + +class _ListenableSelectorHook extends Hook { + const _ListenableSelectorHook(this.listenable, this.selector); + + final Listenable listenable; + final R Function() selector; + + @override + _ListenableSelectorHookState createState() => + _ListenableSelectorHookState(); +} + +class _ListenableSelectorHookState + extends HookState> { + late R _selectorResult = hook.selector(); + + @override + void initHook() { + super.initHook(); + hook.listenable.addListener(_listener); + } + + @override + void didUpdateHook(_ListenableSelectorHook oldHook) { + super.didUpdateHook(oldHook); + + if (hook.selector != oldHook.selector) { + setState(() { + _selectorResult = hook.selector(); + }); + } + + if (hook.listenable != oldHook.listenable) { + oldHook.listenable.removeListener(_listener); + hook.listenable.addListener(_listener); + _selectorResult = hook.selector(); + } + } + + @override + R build(BuildContext context) => _selectorResult; + + void _listener() { + final latestSelectorResult = hook.selector(); + if (_selectorResult != latestSelectorResult) { + setState(() { + _selectorResult = latestSelectorResult; + }); + } + } + + @override + void dispose() { + hook.listenable.removeListener(_listener); + } + + @override + String get debugLabel => 'useListenableSelector<$R>'; + + @override + bool get debugSkipValue => true; +} diff --git a/packages/flutter_hooks/test/use_listenable_selector_test.dart b/packages/flutter_hooks/test/use_listenable_selector_test.dart new file mode 100644 index 00000000..433d7717 --- /dev/null +++ b/packages/flutter_hooks/test/use_listenable_selector_test.dart @@ -0,0 +1,173 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; + +import 'mock.dart'; + +void main() { + testWidgets('debugFillProperties', (tester) async { + await tester.pumpWidget( + HookBuilder( + builder: (context) { + final listenable = ValueNotifier(42); + useListenableSelector(listenable, () => listenable.value.isOdd); + return const SizedBox(); + }, + ), + ); + + final element = tester.element(find.byType(HookBuilder)); + + expect( + element + .toDiagnosticsNode(style: DiagnosticsTreeStyle.offstage) + .toStringDeep(), + equalsIgnoringHashCodes( + 'HookBuilder\n' + ' │ useListenableSelector\n' + ' └SizedBox(renderObject: RenderConstrainedBox#00000)\n' + '', + ), + ); + }); + + testWidgets('basic', (tester) async { + final listenable = ValueNotifier(0); + // ignore: prefer_function_declarations_over_variables + final isOddSelector = () => listenable.value.isOdd; + var isOdd = listenable.value.isOdd; + + await tester.pumpWidget( + HookBuilder( + builder: (context) { + isOdd = useListenableSelector(listenable, isOddSelector); + return Container(); + }, + ), + ); + + final element = tester.firstElement(find.byType(HookBuilder)); + + // ignore: invalid_use_of_protected_member + expect(listenable.hasListeners, true); + expect(listenable.value, 0); + expect(isOdd, false); + expect(element.dirty, false); + + listenable.value++; + + expect(element.dirty, true); + expect(listenable.value, 1); + + await tester.pump(); + + expect(isOdd, true); + expect(element.dirty, false); + + listenable.value++; + + expect(element.dirty, true); + + await tester.pump(); + + expect(listenable.value, 2); + expect(isOdd, false); + + listenable.value = listenable.value + 2; + + expect(element.dirty, false); + + await tester.pump(); + + expect(listenable.value, 4); + expect(isOdd, false); + + listenable.dispose(); + }); + + testWidgets('update selector', (tester) async { + final listenable = ValueNotifier(0); + var isOdd = false; + // ignore: prefer_function_declarations_over_variables + bool isOddSelector() => listenable.value.isOdd; + var isEven = false; + bool isEvenSelector() => listenable.value.isEven; + + await tester.pumpWidget( + HookBuilder( + builder: (context) { + isOdd = useListenableSelector(listenable, isOddSelector); + return Container(); + }, + ), + ); + + final element = tester.firstElement(find.byType(HookBuilder)); + + // ignore: invalid_use_of_protected_member + expect(listenable.hasListeners, true); + expect(listenable.value, 0); + expect(isOdd, false); + expect(element.dirty, false); + + listenable.value++; + + expect(element.dirty, true); + expect(listenable.value, 1); + + await tester.pump(); + + expect(isOdd, true); + expect(element.dirty, false); + + await tester.pumpWidget( + HookBuilder( + builder: (context) { + isEven = useListenableSelector(listenable, isEvenSelector); + return Container(); + }, + ), + ); + + expect(listenable.value, 1); + expect(isEven, false); + + listenable.dispose(); + }); + + testWidgets('update listenable', (tester) async { + var listenable = ValueNotifier(0); + bool isOddSelector() => listenable.value.isOdd; + var isOdd = false; + + await tester.pumpWidget( + HookBuilder( + builder: (context) { + isOdd = useListenableSelector(listenable, isOddSelector); + return Container(); + }, + ), + ); + + expect(isOdd, false); + + final previousListenable = listenable; + listenable = ValueNotifier(1); + + await tester.pumpWidget( + HookBuilder( + builder: (context) { + isOdd = useListenableSelector(listenable, isOddSelector); + return Container(); + }, + ), + ); + + // ignore: invalid_use_of_protected_member + expect(previousListenable.hasListeners, false); + expect(isOdd, true); + + listenable.dispose(); + previousListenable.dispose(); + }); +} From 1ee979c0ddce3a48751d36974df1c312e001dce0 Mon Sep 17 00:00:00 2001 From: Nicolas Schlecker Date: Fri, 17 Jun 2022 17:37:00 +0200 Subject: [PATCH 270/384] add useAutomaticKeepAlive hook (#297) --- README.md | 1 + packages/flutter_hooks/lib/src/hooks.dart | 1 + .../flutter_hooks/lib/src/keep_alive.dart | 70 ++++++++++ .../test/use_automatic_keep_alive_test.dart | 127 ++++++++++++++++++ 4 files changed, 199 insertions(+) create mode 100644 packages/flutter_hooks/lib/src/keep_alive.dart create mode 100644 packages/flutter_hooks/test/use_automatic_keep_alive_test.dart diff --git a/README.md b/README.md index a97554fe..b9051d1d 100644 --- a/README.md +++ b/README.md @@ -350,6 +350,7 @@ A series of hooks with no particular theme. | [useOnAppLifecycleStateChange](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useOnAppLifecycleStateChange.html) | Listens to `AppLifecycleState` changes and triggers a callback on change. | | [useTransformationController](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useTransformationController.html) | Creates and disposes a `TransformationController`. | | [useIsMounted](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useIsMounted.html) | An equivalent to `State.mounted` for hooks. | +| [useAutomaticKeepAlive](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useAutomaticKeepAlive.html) | An equivalent to the `AutomaticKeepAlive` widget for hooks. | ## Contributions diff --git a/packages/flutter_hooks/lib/src/hooks.dart b/packages/flutter_hooks/lib/src/hooks.dart index a102f8e4..78a4dd9e 100644 --- a/packages/flutter_hooks/lib/src/hooks.dart +++ b/packages/flutter_hooks/lib/src/hooks.dart @@ -19,4 +19,5 @@ part 'scroll_controller.dart'; part 'page_controller.dart'; part 'widgets_binding_observer.dart'; part 'transformation_controller.dart'; +part 'keep_alive.dart'; part 'listenable_selector.dart'; diff --git a/packages/flutter_hooks/lib/src/keep_alive.dart b/packages/flutter_hooks/lib/src/keep_alive.dart new file mode 100644 index 00000000..5f915b25 --- /dev/null +++ b/packages/flutter_hooks/lib/src/keep_alive.dart @@ -0,0 +1,70 @@ +part of 'hooks.dart'; + +/// Mark a widget using this hook as needing to stay alive even when it's in a +/// lazy list that would otherwise remove it. +/// +/// See also: +/// - [AutomaticKeepAlive] +/// - [KeepAlive] +void useAutomaticKeepAlive({ + bool wantKeepAlive = true, +}) { + use(_AutomaticKeepAliveHook( + wantKeepAlive: wantKeepAlive, + )); +} + +class _AutomaticKeepAliveHook extends Hook { + const _AutomaticKeepAliveHook({required this.wantKeepAlive}); + + final bool wantKeepAlive; + + @override + HookState createState() => + _AutomaticKeepAliveHookState(); +} + +class _AutomaticKeepAliveHookState + extends HookState { + KeepAliveHandle? _keepAliveHandle; + + void _ensureKeepAlive() { + _keepAliveHandle = KeepAliveHandle(); + KeepAliveNotification(_keepAliveHandle!).dispatch(context); + } + + void _releaseKeepAlive() { + _keepAliveHandle?.release(); + _keepAliveHandle = null; + } + + @override + void initHook() { + super.initHook(); + + if (hook.wantKeepAlive) { + _ensureKeepAlive(); + } + } + + @override + void build(BuildContext context) { + if (hook.wantKeepAlive && _keepAliveHandle == null) { + _ensureKeepAlive(); + } + } + + @override + void deactivate() { + if (_keepAliveHandle != null) { + _releaseKeepAlive(); + } + super.deactivate(); + } + + @override + Object? get debugValue => _keepAliveHandle; + + @override + String get debugLabel => 'useAutomaticKeepAlive'; +} diff --git a/packages/flutter_hooks/test/use_automatic_keep_alive_test.dart b/packages/flutter_hooks/test/use_automatic_keep_alive_test.dart new file mode 100644 index 00000000..00f92e89 --- /dev/null +++ b/packages/flutter_hooks/test/use_automatic_keep_alive_test.dart @@ -0,0 +1,127 @@ +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() { + group('useAutomaticKeepAlive', () { + testWidgets('debugFillProperties', (tester) async { + await tester.pumpWidget( + HookBuilder(builder: (context) { + useAutomaticKeepAlive(); + return const SizedBox(); + }), + ); + + await tester.pump(); + + final element = tester.element(find.byType(HookBuilder)); + + expect( + element + .toDiagnosticsNode(style: DiagnosticsTreeStyle.offstage) + .toStringDeep(), + equalsIgnoringHashCodes( + 'HookBuilder\n' + " │ useAutomaticKeepAlive: Instance of 'KeepAliveHandle'\n" + ' └SizedBox(renderObject: RenderConstrainedBox#00000)\n', + ), + ); + }); + + testWidgets('keeps widget alive in a TabView', (tester) async { + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: DefaultTabController( + length: 2, + child: TabBarView( + children: [ + HookBuilder(builder: (context) { + useAutomaticKeepAlive(); + return Container(); + }), + Container(), + ], + ), + ), + ), + ); + await tester.pump(); + + final findKeepAlive = find.byType(AutomaticKeepAlive); + final keepAlive = tester.element(findKeepAlive); + + expect(findKeepAlive, findsOneWidget); + expect( + keepAlive + .toDiagnosticsNode(style: DiagnosticsTreeStyle.shallow) + .toStringDeep(), + equalsIgnoringHashCodes( + 'AutomaticKeepAlive:\n' + ' state: _AutomaticKeepAliveState#00000(keeping subtree alive,\n' + ' handles: 1 active client)\n', + ), + ); + }); + + testWidgets( + 'start keep alive when wantKeepAlive changes to true', + (tester) async { + final keepAliveNotifier = ValueNotifier(false); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: DefaultTabController( + length: 2, + child: TabBarView( + children: [ + HookBuilder(builder: (context) { + final wantKeepAlive = useValueListenable(keepAliveNotifier); + useAutomaticKeepAlive(wantKeepAlive: wantKeepAlive); + return Container(); + }), + Container(), + ], + ), + ), + ), + ); + await tester.pump(); + + final findKeepAlive = find.byType(AutomaticKeepAlive); + final keepAlive = tester.element(findKeepAlive); + + expect(findKeepAlive, findsOneWidget); + expect( + keepAlive + .toDiagnosticsNode(style: DiagnosticsTreeStyle.shallow) + .toStringDeep(), + equalsIgnoringHashCodes( + 'AutomaticKeepAlive:\n' + ' state: _AutomaticKeepAliveState#00000(handles: no notifications\n' + ' ever received)\n', + ), + ); + + keepAliveNotifier.value = true; + await tester.pump(); + + expect(findKeepAlive, findsOneWidget); + expect( + keepAlive + .toDiagnosticsNode(style: DiagnosticsTreeStyle.shallow) + .toStringDeep(), + equalsIgnoringHashCodes( + 'AutomaticKeepAlive:\n' + ' state: _AutomaticKeepAliveState#00000(keeping subtree alive,\n' + ' handles: 1 active client)\n', + ), + ); + }, + ); + }); +} From c4f770db9227b3dfabc36a9a70ea51c94387f460 Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Fri, 17 Jun 2022 17:38:17 +0200 Subject: [PATCH 271/384] Changelog --- packages/flutter_hooks/CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/flutter_hooks/CHANGELOG.md b/packages/flutter_hooks/CHANGELOG.md index 4628d56a..23589dbc 100644 --- a/packages/flutter_hooks/CHANGELOG.md +++ b/packages/flutter_hooks/CHANGELOG.md @@ -1,7 +1,8 @@ ## Unreleased minor -Added `useListenableSelector`, similar to `useListenable`, for listening to a +- Added `useListenableSelector`, similar to `useListenable`, for listening to a `Listenable` but rebuilding the widget only if a certain data has changed (thanks to @ronnieeeeee) +- Added `useAutomaticKeepAlive` (thanks to @DevNico) ## 0.18.4 From c09a946bc45dcf7c2108e23f7055a9caa69df9ca Mon Sep 17 00:00:00 2001 From: Ahmed Elsayed Date: Fri, 17 Jun 2022 17:40:53 +0200 Subject: [PATCH 272/384] Add PlatformBrightness hook (#291) --- README.md | 1 + packages/flutter_hooks/CHANGELOG.md | 1 + packages/flutter_hooks/lib/src/hooks.dart | 3 +- .../lib/src/platform_brightness.dart | 69 +++++++++++++++++ .../test/use_platform_brightness_test.dart | 74 +++++++++++++++++++ 5 files changed, 147 insertions(+), 1 deletion(-) create mode 100644 packages/flutter_hooks/lib/src/platform_brightness.dart create mode 100644 packages/flutter_hooks/test/use_platform_brightness_test.dart diff --git a/README.md b/README.md index b9051d1d..ad69f566 100644 --- a/README.md +++ b/README.md @@ -351,6 +351,7 @@ A series of hooks with no particular theme. | [useTransformationController](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useTransformationController.html) | Creates and disposes a `TransformationController`. | | [useIsMounted](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useIsMounted.html) | An equivalent to `State.mounted` for hooks. | | [useAutomaticKeepAlive](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useAutomaticKeepAlive.html) | An equivalent to the `AutomaticKeepAlive` widget for hooks. | +| [useOnPlatformBrightnessChange](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useOnPlatformBrightnessChange.html) | Listens to platform `Brightness` changes and triggers a callback on change.| ## Contributions diff --git a/packages/flutter_hooks/CHANGELOG.md b/packages/flutter_hooks/CHANGELOG.md index 23589dbc..51349fc1 100644 --- a/packages/flutter_hooks/CHANGELOG.md +++ b/packages/flutter_hooks/CHANGELOG.md @@ -3,6 +3,7 @@ - Added `useListenableSelector`, similar to `useListenable`, for listening to a `Listenable` but rebuilding the widget only if a certain data has changed (thanks to @ronnieeeeee) - Added `useAutomaticKeepAlive` (thanks to @DevNico) +- Added `usePlatformBrightness` and `useOnPlatformBrightnessChange` to interact with platform `Brightness` (thanks to @AhmedLSayed9) ## 0.18.4 diff --git a/packages/flutter_hooks/lib/src/hooks.dart b/packages/flutter_hooks/lib/src/hooks.dart index 78a4dd9e..b9152b31 100644 --- a/packages/flutter_hooks/lib/src/hooks.dart +++ b/packages/flutter_hooks/lib/src/hooks.dart @@ -1,7 +1,7 @@ import 'dart:async'; import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart' show TabController; +import 'package:flutter/material.dart' show Brightness, TabController; import 'package:flutter/scheduler.dart'; import 'package:flutter/widgets.dart'; @@ -19,5 +19,6 @@ 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'; diff --git a/packages/flutter_hooks/lib/src/platform_brightness.dart b/packages/flutter_hooks/lib/src/platform_brightness.dart new file mode 100644 index 00000000..66cf5636 --- /dev/null +++ b/packages/flutter_hooks/lib/src/platform_brightness.dart @@ -0,0 +1,69 @@ +part of 'hooks.dart'; + +/// A callback triggered when the platform brightness changes. +typedef BrightnessCallback = FutureOr Function( + Brightness previous, + Brightness current, +); + +/// Returns the current platform [Brightness] value and rebuilds the widget when it changes. +Brightness usePlatformBrightness() { + return use(const _PlatformBrightnessHook(rebuildOnChange: true)); +} + +/// Listens to the platform [Brightness]. +void useOnPlatformBrightnessChange(BrightnessCallback onBrightnessChange) { + return use(_PlatformBrightnessHook(onBrightnessChange: onBrightnessChange)); +} + +class _PlatformBrightnessHook extends Hook { + const _PlatformBrightnessHook({ + this.rebuildOnChange = false, + this.onBrightnessChange, + }) : super(); + + final bool rebuildOnChange; + final BrightnessCallback? onBrightnessChange; + + @override + _PlatformBrightnessState createState() => _PlatformBrightnessState(); +} + +class _PlatformBrightnessState + extends HookState + with + // ignore: prefer_mixin + WidgetsBindingObserver { + late Brightness _brightness; + + @override + String? get debugLabel => 'usePlatformBrightness'; + + @override + void initHook() { + super.initHook(); + _brightness = WidgetsBinding.instance.window.platformBrightness; + WidgetsBinding.instance.addObserver(this); + } + + @override + Brightness build(BuildContext context) => _brightness; + + @override + void dispose() { + WidgetsBinding.instance.removeObserver(this); + super.dispose(); + } + + @override + void didChangePlatformBrightness() { + super.didChangePlatformBrightness(); + final _previous = _brightness; + _brightness = WidgetsBinding.instance.window.platformBrightness; + hook.onBrightnessChange?.call(_previous, _brightness); + + if (hook.rebuildOnChange) { + setState(() {}); + } + } +} diff --git a/packages/flutter_hooks/test/use_platform_brightness_test.dart b/packages/flutter_hooks/test/use_platform_brightness_test.dart new file mode 100644 index 00000000..b2ff32e1 --- /dev/null +++ b/packages/flutter_hooks/test/use_platform_brightness_test.dart @@ -0,0 +1,74 @@ +import 'dart:ui'; + +import 'package:flutter/widgets.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; + +import 'mock.dart'; + +void main() { + group('usePlatformBrightness', () { + testWidgets('returns initial value and rebuild widgets on change', + (tester) async { + final binding = tester.binding; + binding.platformDispatcher.platformBrightnessTestValue = Brightness.light; + + await tester.pumpWidget( + HookBuilder( + builder: (context) { + final brightness = usePlatformBrightness(); + return Text('$brightness', textDirection: TextDirection.ltr); + }, + ), + ); + + expect(find.text('Brightness.light'), findsOneWidget); + + binding.platformDispatcher.platformBrightnessTestValue = Brightness.dark; + await tester.pump(); + + expect(find.text('Brightness.dark'), findsOneWidget); + }); + }); + + group('useOnPlatformBrightnessChange', () { + testWidgets( + 'sends previous and new value on change, without rebuilding widgets', + (tester) async { + final binding = tester.binding; + binding.platformDispatcher.platformBrightnessTestValue = Brightness.light; + var buildCount = 0; + final listener = PlatformBrightnessListener(); + + await tester.pumpWidget( + HookBuilder( + builder: (context) { + buildCount++; + useOnPlatformBrightnessChange(listener); + return Container(); + }, + ), + ); + + expect(buildCount, 1); + verifyZeroInteractions(listener); + + binding.platformDispatcher.platformBrightnessTestValue = Brightness.dark; + await tester.pump(); + + expect(buildCount, 1); + verify(listener(Brightness.light, Brightness.dark)); + verifyNoMoreInteractions(listener); + + binding.platformDispatcher.platformBrightnessTestValue = Brightness.light; + await tester.pump(); + + expect(buildCount, 1); + verify(listener(Brightness.dark, Brightness.light)); + verifyNoMoreInteractions(listener); + }); + }); +} + +class PlatformBrightnessListener extends Mock { + void call(Brightness previous, Brightness current); +} From 3156ec70b126d0685bc667c7db4230838e1aa8dc Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Fri, 17 Jun 2022 17:41:34 +0200 Subject: [PATCH 273/384] 0.18.5 --- packages/flutter_hooks/CHANGELOG.md | 2 +- packages/flutter_hooks/pubspec.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/flutter_hooks/CHANGELOG.md b/packages/flutter_hooks/CHANGELOG.md index 51349fc1..c38a6975 100644 --- a/packages/flutter_hooks/CHANGELOG.md +++ b/packages/flutter_hooks/CHANGELOG.md @@ -1,4 +1,4 @@ -## Unreleased minor +## 0.18.5 - Added `useListenableSelector`, similar to `useListenable`, for listening to a `Listenable` but rebuilding the widget only if a certain data has changed (thanks to @ronnieeeeee) diff --git a/packages/flutter_hooks/pubspec.yaml b/packages/flutter_hooks/pubspec.yaml index 515f7062..0119bcfc 100644 --- a/packages/flutter_hooks/pubspec.yaml +++ b/packages/flutter_hooks/pubspec.yaml @@ -1,7 +1,7 @@ name: flutter_hooks description: A flutter implementation of React hooks. It adds a new kind of widget with enhanced code reuse. homepage: https://github.com/rrousselGit/flutter_hooks -version: 0.18.4 +version: 0.18.5 environment: sdk: ">=2.17.0 <3.0.0" From 8d9a5dbe5879f05b1806688853904dd2892827c7 Mon Sep 17 00:00:00 2001 From: Nicolas Schlecker Date: Fri, 24 Jun 2022 10:30:37 +0200 Subject: [PATCH 274/384] fix link to example main.dart on pub.dev example tab (#311) --- packages/flutter_hooks/example/pubspec.lock | 2 +- packages/flutter_hooks/pubspec.yaml | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/flutter_hooks/example/pubspec.lock b/packages/flutter_hooks/example/pubspec.lock index 8a0495a6..7a03b478 100644 --- a/packages/flutter_hooks/example/pubspec.lock +++ b/packages/flutter_hooks/example/pubspec.lock @@ -201,7 +201,7 @@ packages: path: ".." relative: true source: path - version: "0.18.4" + version: "0.18.5" flutter_test: dependency: "direct dev" description: flutter diff --git a/packages/flutter_hooks/pubspec.yaml b/packages/flutter_hooks/pubspec.yaml index 0119bcfc..b002e34f 100644 --- a/packages/flutter_hooks/pubspec.yaml +++ b/packages/flutter_hooks/pubspec.yaml @@ -1,6 +1,8 @@ name: flutter_hooks description: A flutter implementation of React hooks. It adds a new kind of widget with enhanced code reuse. homepage: https://github.com/rrousselGit/flutter_hooks +repository: https://github.com/rrousselGit/flutter_hooks/tree/master/packages/flutter_hooks +issue_tracker: https://github.com/rrousselGit/flutter_hooks/issues version: 0.18.5 environment: From f58f3eb1f6d6a74e32c29d29d43db68f5fab71df Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Fri, 24 Jun 2022 10:31:56 +0200 Subject: [PATCH 275/384] 0.18.5+1 --- packages/flutter_hooks/CHANGELOG.md | 4 ++++ packages/flutter_hooks/pubspec.yaml | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/flutter_hooks/CHANGELOG.md b/packages/flutter_hooks/CHANGELOG.md index c38a6975..848c1c6a 100644 --- a/packages/flutter_hooks/CHANGELOG.md +++ b/packages/flutter_hooks/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.18.5+1 + +Update links to the repository + ## 0.18.5 - Added `useListenableSelector`, similar to `useListenable`, for listening to a diff --git a/packages/flutter_hooks/pubspec.yaml b/packages/flutter_hooks/pubspec.yaml index b002e34f..765bfc41 100644 --- a/packages/flutter_hooks/pubspec.yaml +++ b/packages/flutter_hooks/pubspec.yaml @@ -3,7 +3,7 @@ description: A flutter implementation of React hooks. It adds a new kind of widg homepage: https://github.com/rrousselGit/flutter_hooks repository: https://github.com/rrousselGit/flutter_hooks/tree/master/packages/flutter_hooks issue_tracker: https://github.com/rrousselGit/flutter_hooks/issues -version: 0.18.5 +version: 0.18.5+1 environment: sdk: ">=2.17.0 <3.0.0" From f9326c8750d3d77b9488f2f6fa291e61f2abe2fb Mon Sep 17 00:00:00 2001 From: Justin White Date: Tue, 5 Jul 2022 04:38:10 -0400 Subject: [PATCH 276/384] Fix use_transformation_controller test file (#314) --- packages/flutter_hooks/example/pubspec.lock | 2 +- ...roller.dart => use_transformation_controller_test.dart} | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) rename packages/flutter_hooks/test/{use_transformation_controller.dart => use_transformation_controller_test.dart} (90%) diff --git a/packages/flutter_hooks/example/pubspec.lock b/packages/flutter_hooks/example/pubspec.lock index 7a03b478..12c25904 100644 --- a/packages/flutter_hooks/example/pubspec.lock +++ b/packages/flutter_hooks/example/pubspec.lock @@ -201,7 +201,7 @@ packages: path: ".." relative: true source: path - version: "0.18.5" + version: "0.18.5+1" flutter_test: dependency: "direct dev" description: flutter diff --git a/packages/flutter_hooks/test/use_transformation_controller.dart b/packages/flutter_hooks/test/use_transformation_controller_test.dart similarity index 90% rename from packages/flutter_hooks/test/use_transformation_controller.dart rename to packages/flutter_hooks/test/use_transformation_controller_test.dart index 2b3aa35f..4df11c08 100644 --- a/packages/flutter_hooks/test/use_transformation_controller.dart +++ b/packages/flutter_hooks/test/use_transformation_controller_test.dart @@ -22,7 +22,12 @@ void main() { .toStringDeep(), equalsIgnoringHashCodes( 'HookBuilder\n' - ' │ useTransformationController: TransformationController#00000(no clients)\n' + ' │ useTransformationController:\n' + ' │ TransformationController#00000([0] 1.0,0.0,0.0,0.0\n' + ' │ [1] 0.0,1.0,0.0,0.0\n' + ' │ [2] 0.0,0.0,1.0,0.0\n' + ' │ [3] 0.0,0.0,0.0,1.0\n' + ' │ )\n' ' └SizedBox(renderObject: RenderConstrainedBox#00000)\n', ), ); From 72192825b77191721aa882dbd23228b2e18e9662 Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Mon, 16 Jan 2023 11:34:45 +0100 Subject: [PATCH 277/384] Upgrade flutter_hooks example --- packages/flutter_hooks/example/pubspec.lock | 381 +++++++++++--------- packages/flutter_hooks/example/pubspec.yaml | 4 +- 2 files changed, 221 insertions(+), 164 deletions(-) diff --git a/packages/flutter_hooks/example/pubspec.lock b/packages/flutter_hooks/example/pubspec.lock index 12c25904..de115497 100644 --- a/packages/flutter_hooks/example/pubspec.lock +++ b/packages/flutter_hooks/example/pubspec.lock @@ -5,189 +5,208 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - url: "https://pub.dartlang.org" + sha256: "0c80aeab9bc807ab10022cd3b2f4cf2ecdf231949dc1ddd9442406a003f19201" + url: "https://pub.dev" source: hosted - version: "40.0.0" + version: "52.0.0" analyzer: dependency: transitive description: name: analyzer - url: "https://pub.dartlang.org" + sha256: cd8ee83568a77f3ae6b913a36093a1c9b1264e7cb7f834d9ddd2311dade9c1f4 + url: "https://pub.dev" source: hosted - version: "4.1.0" + version: "5.4.0" args: dependency: transitive description: name: args - url: "https://pub.dartlang.org" + sha256: b003c3098049a51720352d219b0bb5f219b60fbfb68e7a4748139a06a5676515 + url: "https://pub.dev" source: hosted version: "2.3.1" async: dependency: transitive description: name: async - url: "https://pub.dartlang.org" + sha256: bfe67ef28df125b7dddcea62755991f807aa39a2492a23e1550161692950bbe0 + url: "https://pub.dev" source: hosted - version: "2.8.2" + version: "2.10.0" boolean_selector: dependency: transitive description: name: boolean_selector - url: "https://pub.dartlang.org" + sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" + url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.1" build: dependency: transitive description: name: build - url: "https://pub.dartlang.org" + sha256: "3fbda25365741f8251b39f3917fb3c8e286a96fd068a5a242e11c2012d495777" + url: "https://pub.dev" source: hosted - version: "2.3.0" + version: "2.3.1" build_config: dependency: transitive description: name: build_config - url: "https://pub.dartlang.org" + sha256: bf80fcfb46a29945b423bd9aad884590fb1dc69b330a4d4700cac476af1708d1 + url: "https://pub.dev" source: hosted - version: "1.0.0" + version: "1.1.1" build_daemon: dependency: transitive description: name: build_daemon - url: "https://pub.dartlang.org" + sha256: "6bc5544ea6ce4428266e7ea680e945c68806c4aae2da0eb5e9ccf38df8d6acbf" + url: "https://pub.dev" source: hosted version: "3.1.0" build_resolvers: dependency: transitive description: name: build_resolvers - url: "https://pub.dartlang.org" + sha256: "7c35a3a7868626257d8aee47b51c26b9dba11eaddf3431117ed2744951416aab" + url: "https://pub.dev" source: hosted - version: "2.0.9" + version: "2.1.0" build_runner: dependency: "direct dev" description: name: build_runner - url: "https://pub.dartlang.org" + sha256: b0a8a7b8a76c493e85f1b84bffa0588859a06197863dba8c9036b15581fd9727 + url: "https://pub.dev" source: hosted - version: "2.1.11" + version: "2.3.3" build_runner_core: dependency: transitive description: name: build_runner_core - url: "https://pub.dartlang.org" + sha256: "14febe0f5bac5ae474117a36099b4de6f1dbc52df6c5e55534b3da9591bf4292" + url: "https://pub.dev" source: hosted - version: "7.2.3" + version: "7.2.7" built_collection: dependency: "direct main" description: name: built_collection - url: "https://pub.dartlang.org" + sha256: "376e3dd27b51ea877c28d525560790aee2e6fbb5f20e2f85d5081027d94e2100" + url: "https://pub.dev" source: hosted version: "5.1.1" built_value: dependency: "direct main" description: name: built_value - url: "https://pub.dartlang.org" + sha256: "169565c8ad06adb760c3645bf71f00bff161b00002cace266cad42c5d22a7725" + url: "https://pub.dev" source: hosted - version: "8.3.3" + version: "8.4.3" built_value_generator: dependency: "direct dev" description: name: built_value_generator - url: "https://pub.dartlang.org" + sha256: a5b989bb5fa7c22c481c3b6f8195703818f047be0b65ca9b1358770c475709e3 + url: "https://pub.dev" source: hosted - version: "8.3.3" + version: "8.4.3" characters: dependency: transitive description: name: characters - url: "https://pub.dartlang.org" + sha256: e6a326c8af69605aec75ed6c187d06b349707a27fbff8222ca9cc2cff167975c + url: "https://pub.dev" source: hosted - version: "1.2.0" - charcode: - dependency: transitive - description: - name: charcode - url: "https://pub.dartlang.org" - source: hosted - version: "1.3.1" + version: "1.2.1" checked_yaml: dependency: transitive description: name: checked_yaml - url: "https://pub.dartlang.org" + sha256: "3d1505d91afa809d177efd4eed5bb0eb65805097a1463abdd2add076effae311" + url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "2.0.2" clock: dependency: transitive description: name: clock - url: "https://pub.dartlang.org" + sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf + url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.1.1" code_builder: dependency: transitive description: name: code_builder - url: "https://pub.dartlang.org" + sha256: "0d43dd1288fd145de1ecc9a3948ad4a6d5a82f0a14c4fdd0892260787d975cbe" + url: "https://pub.dev" source: hosted - version: "4.1.0" + version: "4.4.0" collection: dependency: transitive description: name: collection - url: "https://pub.dartlang.org" + sha256: cfc915e6923fe5ce6e153b0723c753045de46de1b4d63771530504004a45fae0 + url: "https://pub.dev" source: hosted - version: "1.16.0" + version: "1.17.0" convert: dependency: transitive description: name: convert - url: "https://pub.dartlang.org" + sha256: "0f08b14755d163f6e2134cb58222dd25ea2a2ee8a195e53983d57c075324d592" + url: "https://pub.dev" source: hosted - version: "3.0.2" + version: "3.1.1" crypto: dependency: transitive description: name: crypto - url: "https://pub.dartlang.org" + sha256: aa274aa7774f8964e4f4f38cc994db7b6158dd36e9187aaceaddc994b35c6c67 + url: "https://pub.dev" source: hosted version: "3.0.2" dart_style: dependency: transitive description: name: dart_style - url: "https://pub.dartlang.org" + sha256: "7a03456c3490394c8e7665890333e91ae8a49be43542b616e414449ac358acd4" + url: "https://pub.dev" source: hosted - version: "2.2.3" + version: "2.2.4" fake_async: dependency: transitive description: name: fake_async - url: "https://pub.dartlang.org" + sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" + url: "https://pub.dev" source: hosted - version: "1.3.0" + version: "1.3.1" ffi: dependency: transitive description: name: ffi - url: "https://pub.dartlang.org" + sha256: a38574032c5f1dd06c4aee541789906c12ccaab8ba01446e800d9c5b79c4a978 + url: "https://pub.dev" source: hosted - version: "2.0.0" + version: "2.0.1" file: dependency: transitive description: name: file - url: "https://pub.dartlang.org" + sha256: "1b92bec4fc2a72f59a8e15af5f52cd441e4a7860b49499d69dfa817af20e925d" + url: "https://pub.dev" source: hosted - version: "6.1.2" + version: "6.1.4" fixnum: dependency: transitive description: name: fixnum - url: "https://pub.dartlang.org" + sha256: "04be3e934c52e082558cc9ee21f42f5c1cd7a1262f4c63cd0357c08d5bba81ec" + url: "https://pub.dev" source: hosted version: "1.0.1" flutter: @@ -216,268 +235,290 @@ packages: dependency: transitive description: name: frontend_server_client - url: "https://pub.dartlang.org" + sha256: "408e3ca148b31c20282ad6f37ebfa6f4bdc8fede5b74bc2f08d9d92b55db3612" + url: "https://pub.dev" source: hosted - version: "2.1.3" + version: "3.2.0" glob: dependency: transitive description: name: glob - url: "https://pub.dartlang.org" + sha256: "4515b5b6ddb505ebdd242a5f2cc5d22d3d6a80013789debfbda7777f47ea308c" + url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.1" graphs: dependency: transitive description: name: graphs - url: "https://pub.dartlang.org" + sha256: f9e130f3259f52d26f0cfc0e964513796dafed572fa52e45d2f8d6ca14db39b2 + url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.2.0" http: dependency: "direct main" description: name: http - url: "https://pub.dartlang.org" + sha256: "6aa2946395183537c8b880962d935877325d6a09a2867c3970c05c0fed6ac482" + url: "https://pub.dev" source: hosted - version: "0.13.4" + version: "0.13.5" http_multi_server: dependency: transitive description: name: http_multi_server - url: "https://pub.dartlang.org" + sha256: "97486f20f9c2f7be8f514851703d0119c3596d14ea63227af6f7a481ef2b2f8b" + url: "https://pub.dev" source: hosted version: "3.2.1" http_parser: dependency: transitive description: name: http_parser - url: "https://pub.dartlang.org" + sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" + url: "https://pub.dev" source: hosted - version: "4.0.1" + version: "4.0.2" io: dependency: transitive description: name: io - url: "https://pub.dartlang.org" + sha256: "0d4c73c3653ab85bf696d51a9657604c900a370549196a91f33e4c39af760852" + url: "https://pub.dev" source: hosted version: "1.0.3" js: dependency: transitive description: name: js - url: "https://pub.dartlang.org" + sha256: "323b7c70073cccf6b9b8d8b334be418a3293cfb612a560dc2737160a37bf61bd" + url: "https://pub.dev" source: hosted - version: "0.6.4" + version: "0.6.6" json_annotation: dependency: transitive description: name: json_annotation - url: "https://pub.dartlang.org" + sha256: "3520fa844009431b5d4491a5a778603520cdc399ab3406332dcc50f93547258c" + url: "https://pub.dev" source: hosted - version: "4.5.0" + version: "4.7.0" logging: dependency: transitive description: name: logging - url: "https://pub.dartlang.org" + sha256: c0bbfe94d46aedf9b8b3e695cf3bd48c8e14b35e3b2c639e0aa7755d589ba946 + url: "https://pub.dev" source: hosted - version: "1.0.2" + version: "1.1.0" matcher: dependency: transitive description: name: matcher - url: "https://pub.dartlang.org" + sha256: c94db23593b89766cda57aab9ac311e3616cf87c6fa4e9749df032f66f30dcb8 + url: "https://pub.dev" source: hosted - version: "0.12.11" + version: "0.12.14" material_color_utilities: dependency: transitive description: name: material_color_utilities - url: "https://pub.dartlang.org" + sha256: d92141dc6fe1dad30722f9aa826c7fbc896d021d792f80678280601aff8cf724 + url: "https://pub.dev" source: hosted - version: "0.1.4" + version: "0.2.0" meta: dependency: transitive description: name: meta - url: "https://pub.dartlang.org" + sha256: "6c268b42ed578a53088d834796959e4a1814b5e9e164f147f580a386e5decf42" + url: "https://pub.dev" source: hosted - version: "1.7.0" + version: "1.8.0" mime: dependency: transitive description: name: mime - url: "https://pub.dartlang.org" + sha256: e4ff8e8564c03f255408decd16e7899da1733852a9110a58fe6d1b817684a63e + url: "https://pub.dev" source: hosted - version: "1.0.2" + version: "1.0.4" nested: dependency: transitive description: name: nested - url: "https://pub.dartlang.org" + sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20" + url: "https://pub.dev" source: hosted version: "1.0.0" package_config: dependency: transitive description: name: package_config - url: "https://pub.dartlang.org" + sha256: "1c5b77ccc91e4823a5af61ee74e6b972db1ef98c2ff5a18d3161c982a55448bd" + url: "https://pub.dev" source: hosted version: "2.1.0" path: dependency: transitive description: name: path - url: "https://pub.dartlang.org" + sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917" + url: "https://pub.dev" source: hosted - version: "1.8.1" + version: "1.8.3" path_provider_linux: dependency: transitive description: name: path_provider_linux - url: "https://pub.dartlang.org" + sha256: ab0987bf95bc591da42dffb38c77398fc43309f0b9b894dcc5d6f40c4b26c379 + url: "https://pub.dev" source: hosted version: "2.1.7" path_provider_platform_interface: dependency: transitive description: name: path_provider_platform_interface - url: "https://pub.dartlang.org" + sha256: f0abc8ebd7253741f05488b4813d936b4d07c6bae3e86148a09e342ee4b08e76 + url: "https://pub.dev" source: hosted - version: "2.0.4" + version: "2.0.5" path_provider_windows: dependency: transitive description: name: path_provider_windows - url: "https://pub.dartlang.org" + sha256: bcabbe399d4042b8ee687e17548d5d3f527255253b4a639f5f8d2094a9c2b45c + url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.3" platform: dependency: transitive description: name: platform - url: "https://pub.dartlang.org" + sha256: "4a451831508d7d6ca779f7ac6e212b4023dd5a7d08a27a63da33756410e32b76" + url: "https://pub.dev" source: hosted version: "3.1.0" plugin_platform_interface: dependency: transitive description: name: plugin_platform_interface - url: "https://pub.dartlang.org" + sha256: dbf0f707c78beedc9200146ad3cb0ab4d5da13c246336987be6940f026500d3a + url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.3" pool: dependency: transitive description: name: pool - url: "https://pub.dartlang.org" + sha256: "20fe868b6314b322ea036ba325e6fc0711a22948856475e2c2b6306e8ab39c2a" + url: "https://pub.dev" source: hosted version: "1.5.1" process: dependency: transitive description: name: process - url: "https://pub.dartlang.org" + sha256: "53fd8db9cec1d37b0574e12f07520d582019cb6c44abf5479a01505099a34a09" + url: "https://pub.dev" source: hosted version: "4.2.4" provider: dependency: "direct main" description: name: provider - url: "https://pub.dartlang.org" + sha256: cdbe7530b12ecd9eb455bdaa2fcb8d4dad22e80b8afb4798b41479d5ce26847f + url: "https://pub.dev" source: hosted - version: "4.3.3" + version: "6.0.5" pub_semver: dependency: transitive description: name: pub_semver - url: "https://pub.dartlang.org" + sha256: "307de764d305289ff24ad257ad5c5793ce56d04947599ad68b3baa124105fc17" + url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.3" pubspec_parse: dependency: transitive description: name: pubspec_parse - url: "https://pub.dartlang.org" - source: hosted - version: "1.2.0" - quiver: - dependency: transitive - description: - name: quiver - url: "https://pub.dartlang.org" + sha256: "75f6614d6dde2dc68948dffbaa4fe5dae32cd700eb9fb763fe11dfb45a3c4d0a" + url: "https://pub.dev" source: hosted - version: "2.1.5" + version: "1.2.1" shared_preferences: dependency: "direct main" description: name: shared_preferences - url: "https://pub.dartlang.org" + sha256: "95688ad7fc320f8566f28e2ee91b6743c10b433ccc262f6469f3007f2aa62e78" + url: "https://pub.dev" source: hosted - version: "2.0.15" + version: "2.0.16" shared_preferences_android: dependency: transitive description: name: shared_preferences_android - url: "https://pub.dartlang.org" + sha256: "8e251f3c986002b65fed6396bce81f379fb63c27317d49743cf289fd0fd1ab97" + url: "https://pub.dev" source: hosted - version: "2.0.12" - shared_preferences_ios: + version: "2.0.14" + shared_preferences_foundation: dependency: transitive description: - name: shared_preferences_ios - url: "https://pub.dartlang.org" + name: shared_preferences_foundation + sha256: f451880807c86a6d4591642c8250ee17197cabef8536f072d3c44013dca44e04 + url: "https://pub.dev" source: hosted version: "2.1.1" shared_preferences_linux: dependency: transitive description: name: shared_preferences_linux - url: "https://pub.dartlang.org" + sha256: fbc3cd6826896b66a5f576b025e4f344f780c84ea7f8203097a353370607a2c8 + url: "https://pub.dev" source: hosted - version: "2.1.1" - shared_preferences_macos: - dependency: transitive - description: - name: shared_preferences_macos - url: "https://pub.dartlang.org" - source: hosted - version: "2.0.4" + version: "2.1.2" shared_preferences_platform_interface: dependency: transitive description: name: shared_preferences_platform_interface - url: "https://pub.dartlang.org" + sha256: da9431745ede5ece47bc26d5d73a9d3c6936ef6945c101a5aca46f62e52c1cf3 + url: "https://pub.dev" source: hosted - version: "2.0.0" + version: "2.1.0" shared_preferences_web: dependency: transitive description: name: shared_preferences_web - url: "https://pub.dartlang.org" + sha256: a4b5bc37fe1b368bbc81f953197d55e12f49d0296e7e412dfe2d2d77d6929958 + url: "https://pub.dev" source: hosted version: "2.0.4" shared_preferences_windows: dependency: transitive description: name: shared_preferences_windows - url: "https://pub.dartlang.org" + sha256: "07c274c2115d4d5e4280622abb09f0980e2c5b1fcdc98ae9f59a3bad5bfc1f26" + url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.2" shelf: dependency: transitive description: name: shelf - url: "https://pub.dartlang.org" + sha256: c24a96135a2ccd62c64b69315a14adc5c3419df63b4d7c05832a346fdb73682c + url: "https://pub.dev" source: hosted - version: "1.3.1" + version: "1.4.0" shelf_web_socket: dependency: transitive description: name: shelf_web_socket - url: "https://pub.dartlang.org" + sha256: a988c0e8d8ffbdb8a28aa7ec8e449c260f3deb808781fe1284d22c5bba7156e8 + url: "https://pub.dev" source: hosted - version: "1.0.2" + version: "1.0.3" sky_engine: dependency: transitive description: flutter @@ -487,114 +528,130 @@ packages: dependency: transitive description: name: source_gen - url: "https://pub.dartlang.org" + sha256: "2d79738b6bbf38a43920e2b8d189e9a3ce6cc201f4b8fc76be5e4fe377b1c38d" + url: "https://pub.dev" source: hosted - version: "1.2.2" + version: "1.2.6" source_span: dependency: transitive description: name: source_span - url: "https://pub.dartlang.org" + sha256: dd904f795d4b4f3b870833847c461801f6750a9fa8e61ea5ac53f9422b31f250 + url: "https://pub.dev" source: hosted - version: "1.8.2" + version: "1.9.1" stack_trace: dependency: transitive description: name: stack_trace - url: "https://pub.dartlang.org" + sha256: c3c7d8edb15bee7f0f74debd4b9c5f3c2ea86766fe4178eb2a18eb30a0bdaed5 + url: "https://pub.dev" source: hosted - version: "1.10.0" + version: "1.11.0" stream_channel: dependency: transitive description: name: stream_channel - url: "https://pub.dartlang.org" + sha256: "83615bee9045c1d322bbbd1ba209b7a749c2cbcdcb3fdd1df8eb488b3279c1c8" + url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.1" stream_transform: dependency: transitive description: name: stream_transform - url: "https://pub.dartlang.org" + sha256: "14a00e794c7c11aa145a170587321aedce29769c08d7f58b1d141da75e3b1c6f" + url: "https://pub.dev" source: hosted - version: "2.0.0" + version: "2.1.0" string_scanner: dependency: transitive description: name: string_scanner - url: "https://pub.dartlang.org" + sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" + url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.2.0" term_glyph: dependency: transitive description: name: term_glyph - url: "https://pub.dartlang.org" + sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 + url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.2.1" test_api: dependency: transitive description: name: test_api - url: "https://pub.dartlang.org" + sha256: c9282698e2982b6c3817037554e52f99d4daba493e8028f8112a83d68ccd0b12 + url: "https://pub.dev" source: hosted - version: "0.4.9" + version: "0.4.17" timing: dependency: transitive description: name: timing - url: "https://pub.dartlang.org" + sha256: c386d07d7f5efc613479a7c4d9d64b03710b03cfaa7e8ad5f2bfb295a1f0dfad + url: "https://pub.dev" source: hosted version: "1.0.0" typed_data: dependency: transitive description: name: typed_data - url: "https://pub.dartlang.org" + sha256: "26f87ade979c47a150c9eaab93ccd2bebe70a27dc0b4b29517f2904f04eb11a5" + url: "https://pub.dev" source: hosted version: "1.3.1" vector_math: dependency: transitive description: name: vector_math - url: "https://pub.dartlang.org" + sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" + url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.4" watcher: dependency: transitive description: name: watcher - url: "https://pub.dartlang.org" + sha256: "6a7f46926b01ce81bfc339da6a7f20afbe7733eff9846f6d6a5466aa4c6667c0" + url: "https://pub.dev" source: hosted - version: "1.0.1" + version: "1.0.2" web_socket_channel: dependency: transitive description: name: web_socket_channel - url: "https://pub.dartlang.org" + sha256: ca49c0bc209c687b887f30527fb6a9d80040b072cc2990f34b9bec3e7663101b + url: "https://pub.dev" source: hosted - version: "2.2.0" + version: "2.3.0" win32: dependency: transitive description: name: win32 - url: "https://pub.dartlang.org" + sha256: c9ebe7ee4ab0c2194e65d3a07d8c54c5d00bb001b76081c4a04cdb8448b59e46 + url: "https://pub.dev" source: hosted - version: "2.7.0" + version: "3.1.3" xdg_directories: dependency: transitive description: name: xdg_directories - url: "https://pub.dartlang.org" + sha256: bd512f03919aac5f1313eb8249f223bacf4927031bf60b02601f81f687689e86 + url: "https://pub.dev" source: hosted - version: "0.2.0+1" + version: "0.2.0+3" yaml: dependency: transitive description: name: yaml - url: "https://pub.dartlang.org" + sha256: "23812a9b125b48d4007117254bca50abb6c712352927eece9e155207b1db2370" + url: "https://pub.dev" source: hosted version: "3.1.1" sdks: - dart: ">=2.17.0 <3.0.0" + dart: ">=2.19.0-345.0.dev <3.0.0-200.0.dev" flutter: ">=3.0.0" diff --git a/packages/flutter_hooks/example/pubspec.yaml b/packages/flutter_hooks/example/pubspec.yaml index f0001cf4..7c0a7c60 100644 --- a/packages/flutter_hooks/example/pubspec.yaml +++ b/packages/flutter_hooks/example/pubspec.yaml @@ -5,7 +5,7 @@ publish_to: none version: 1.0.0+1 environment: - sdk: ">=2.7.0 <3.0.0" + sdk: ">=2.7.0 <3.0.0-200.0.dev" dependencies: built_collection: ^5.0.0 @@ -15,7 +15,7 @@ dependencies: flutter_hooks: path: ../ http: ^0.13.4 - provider: ^4.2.0 + provider: ^6.0.5 shared_preferences: ^2.0.0 dev_dependencies: From adcf1c50be27be948a4e16b497df2f5b0196534b Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Wed, 25 Jan 2023 18:54:50 +0100 Subject: [PATCH 278/384] Fix CI (#341) --- packages/flutter_hooks/all_lint_rules.yaml | 1 - packages/flutter_hooks/analysis_options.yaml | 11 +- .../example/analysis_options.yaml | 7 +- packages/flutter_hooks/example/pubspec.lock | 657 ------------------ packages/flutter_hooks/lib/src/animation.dart | 4 +- packages/flutter_hooks/lib/src/framework.dart | 10 +- .../flutter_hooks/lib/src/keep_alive.dart | 2 +- .../flutter_hooks/lib/src/listenable.dart | 3 +- .../flutter_hooks/test/hook_widget_test.dart | 6 +- packages/flutter_hooks/test/mock.dart | 5 +- .../test/use_focus_node_test.dart | 7 +- .../flutter_hooks/test/use_state_test.dart | 6 +- .../test/use_stream_controller_test.dart | 14 +- .../test/use_value_notifier_test.dart | 6 +- 14 files changed, 41 insertions(+), 698 deletions(-) delete mode 100644 packages/flutter_hooks/example/pubspec.lock diff --git a/packages/flutter_hooks/all_lint_rules.yaml b/packages/flutter_hooks/all_lint_rules.yaml index 93d14856..225568ae 100644 --- a/packages/flutter_hooks/all_lint_rules.yaml +++ b/packages/flutter_hooks/all_lint_rules.yaml @@ -8,7 +8,6 @@ linter: - always_use_package_imports - annotate_overrides - avoid_annotating_with_dynamic - - avoid_as - avoid_bool_literals_in_conditional_expressions - avoid_catches_without_on_clauses - avoid_catching_errors diff --git a/packages/flutter_hooks/analysis_options.yaml b/packages/flutter_hooks/analysis_options.yaml index 386215f1..810113f9 100644 --- a/packages/flutter_hooks/analysis_options.yaml +++ b/packages/flutter_hooks/analysis_options.yaml @@ -3,9 +3,10 @@ analyzer: exclude: - "**/*.g.dart" - "**/*.freezed.dart" - strong-mode: - implicit-casts: false - implicit-dynamic: false + language: + strict-casts: true + strict-inference: true + strict-raw-types: true errors: # Otherwise cause the import of all_lint_rules to warn because of some rules conflicts. # We explicitly enabled even conflicting rules and are fixing the conflict @@ -43,10 +44,6 @@ linter: # and `@required Widget child` last. always_put_required_named_parameters_first: false - # `as` is not that bad (especially with the upcoming non-nullable types). - # Explicit exceptions is better than implicit exceptions. - avoid_as: false - # This project doesn't use Flutter-style todos flutter_style_todos: false diff --git a/packages/flutter_hooks/example/analysis_options.yaml b/packages/flutter_hooks/example/analysis_options.yaml index f52cc3f8..41fe64f6 100644 --- a/packages/flutter_hooks/example/analysis_options.yaml +++ b/packages/flutter_hooks/example/analysis_options.yaml @@ -1,8 +1,9 @@ include: ../analysis_options.yaml analyzer: - strong-mode: - implicit-casts: false - implicit-dynamic: false + language: + strict-casts: true + strict-inference: true + strict-raw-types: true errors: todo: error include_file_not_found: ignore diff --git a/packages/flutter_hooks/example/pubspec.lock b/packages/flutter_hooks/example/pubspec.lock deleted file mode 100644 index de115497..00000000 --- a/packages/flutter_hooks/example/pubspec.lock +++ /dev/null @@ -1,657 +0,0 @@ -# Generated by pub -# See https://dart.dev/tools/pub/glossary#lockfile -packages: - _fe_analyzer_shared: - dependency: transitive - description: - name: _fe_analyzer_shared - sha256: "0c80aeab9bc807ab10022cd3b2f4cf2ecdf231949dc1ddd9442406a003f19201" - url: "https://pub.dev" - source: hosted - version: "52.0.0" - analyzer: - dependency: transitive - description: - name: analyzer - sha256: cd8ee83568a77f3ae6b913a36093a1c9b1264e7cb7f834d9ddd2311dade9c1f4 - url: "https://pub.dev" - source: hosted - version: "5.4.0" - args: - dependency: transitive - description: - name: args - sha256: b003c3098049a51720352d219b0bb5f219b60fbfb68e7a4748139a06a5676515 - url: "https://pub.dev" - source: hosted - version: "2.3.1" - async: - dependency: transitive - description: - name: async - sha256: bfe67ef28df125b7dddcea62755991f807aa39a2492a23e1550161692950bbe0 - url: "https://pub.dev" - source: hosted - version: "2.10.0" - boolean_selector: - dependency: transitive - description: - name: boolean_selector - sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" - url: "https://pub.dev" - source: hosted - version: "2.1.1" - build: - dependency: transitive - description: - name: build - sha256: "3fbda25365741f8251b39f3917fb3c8e286a96fd068a5a242e11c2012d495777" - url: "https://pub.dev" - source: hosted - version: "2.3.1" - build_config: - dependency: transitive - description: - name: build_config - sha256: bf80fcfb46a29945b423bd9aad884590fb1dc69b330a4d4700cac476af1708d1 - url: "https://pub.dev" - source: hosted - version: "1.1.1" - build_daemon: - dependency: transitive - description: - name: build_daemon - sha256: "6bc5544ea6ce4428266e7ea680e945c68806c4aae2da0eb5e9ccf38df8d6acbf" - url: "https://pub.dev" - source: hosted - version: "3.1.0" - build_resolvers: - dependency: transitive - description: - name: build_resolvers - sha256: "7c35a3a7868626257d8aee47b51c26b9dba11eaddf3431117ed2744951416aab" - url: "https://pub.dev" - source: hosted - version: "2.1.0" - build_runner: - dependency: "direct dev" - description: - name: build_runner - sha256: b0a8a7b8a76c493e85f1b84bffa0588859a06197863dba8c9036b15581fd9727 - url: "https://pub.dev" - source: hosted - version: "2.3.3" - build_runner_core: - dependency: transitive - description: - name: build_runner_core - sha256: "14febe0f5bac5ae474117a36099b4de6f1dbc52df6c5e55534b3da9591bf4292" - url: "https://pub.dev" - source: hosted - version: "7.2.7" - built_collection: - dependency: "direct main" - description: - name: built_collection - sha256: "376e3dd27b51ea877c28d525560790aee2e6fbb5f20e2f85d5081027d94e2100" - url: "https://pub.dev" - source: hosted - version: "5.1.1" - built_value: - dependency: "direct main" - description: - name: built_value - sha256: "169565c8ad06adb760c3645bf71f00bff161b00002cace266cad42c5d22a7725" - url: "https://pub.dev" - source: hosted - version: "8.4.3" - built_value_generator: - dependency: "direct dev" - description: - name: built_value_generator - sha256: a5b989bb5fa7c22c481c3b6f8195703818f047be0b65ca9b1358770c475709e3 - url: "https://pub.dev" - source: hosted - version: "8.4.3" - characters: - dependency: transitive - description: - name: characters - sha256: e6a326c8af69605aec75ed6c187d06b349707a27fbff8222ca9cc2cff167975c - url: "https://pub.dev" - source: hosted - version: "1.2.1" - checked_yaml: - dependency: transitive - description: - name: checked_yaml - sha256: "3d1505d91afa809d177efd4eed5bb0eb65805097a1463abdd2add076effae311" - url: "https://pub.dev" - source: hosted - version: "2.0.2" - clock: - dependency: transitive - description: - name: clock - sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf - url: "https://pub.dev" - source: hosted - version: "1.1.1" - code_builder: - dependency: transitive - description: - name: code_builder - sha256: "0d43dd1288fd145de1ecc9a3948ad4a6d5a82f0a14c4fdd0892260787d975cbe" - url: "https://pub.dev" - source: hosted - version: "4.4.0" - collection: - dependency: transitive - description: - name: collection - sha256: cfc915e6923fe5ce6e153b0723c753045de46de1b4d63771530504004a45fae0 - url: "https://pub.dev" - source: hosted - version: "1.17.0" - convert: - dependency: transitive - description: - name: convert - sha256: "0f08b14755d163f6e2134cb58222dd25ea2a2ee8a195e53983d57c075324d592" - url: "https://pub.dev" - source: hosted - version: "3.1.1" - crypto: - dependency: transitive - description: - name: crypto - sha256: aa274aa7774f8964e4f4f38cc994db7b6158dd36e9187aaceaddc994b35c6c67 - url: "https://pub.dev" - source: hosted - version: "3.0.2" - dart_style: - dependency: transitive - description: - name: dart_style - sha256: "7a03456c3490394c8e7665890333e91ae8a49be43542b616e414449ac358acd4" - url: "https://pub.dev" - source: hosted - version: "2.2.4" - fake_async: - dependency: transitive - description: - name: fake_async - sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" - url: "https://pub.dev" - source: hosted - version: "1.3.1" - ffi: - dependency: transitive - description: - name: ffi - sha256: a38574032c5f1dd06c4aee541789906c12ccaab8ba01446e800d9c5b79c4a978 - url: "https://pub.dev" - source: hosted - version: "2.0.1" - file: - dependency: transitive - description: - name: file - sha256: "1b92bec4fc2a72f59a8e15af5f52cd441e4a7860b49499d69dfa817af20e925d" - url: "https://pub.dev" - source: hosted - version: "6.1.4" - fixnum: - dependency: transitive - description: - name: fixnum - sha256: "04be3e934c52e082558cc9ee21f42f5c1cd7a1262f4c63cd0357c08d5bba81ec" - url: "https://pub.dev" - source: hosted - version: "1.0.1" - flutter: - dependency: "direct main" - description: flutter - source: sdk - version: "0.0.0" - flutter_hooks: - dependency: "direct main" - description: - path: ".." - relative: true - source: path - version: "0.18.5+1" - flutter_test: - dependency: "direct dev" - description: flutter - source: sdk - version: "0.0.0" - flutter_web_plugins: - dependency: transitive - description: flutter - source: sdk - version: "0.0.0" - frontend_server_client: - dependency: transitive - description: - name: frontend_server_client - sha256: "408e3ca148b31c20282ad6f37ebfa6f4bdc8fede5b74bc2f08d9d92b55db3612" - url: "https://pub.dev" - source: hosted - version: "3.2.0" - glob: - dependency: transitive - description: - name: glob - sha256: "4515b5b6ddb505ebdd242a5f2cc5d22d3d6a80013789debfbda7777f47ea308c" - url: "https://pub.dev" - source: hosted - version: "2.1.1" - graphs: - dependency: transitive - description: - name: graphs - sha256: f9e130f3259f52d26f0cfc0e964513796dafed572fa52e45d2f8d6ca14db39b2 - url: "https://pub.dev" - source: hosted - version: "2.2.0" - http: - dependency: "direct main" - description: - name: http - sha256: "6aa2946395183537c8b880962d935877325d6a09a2867c3970c05c0fed6ac482" - url: "https://pub.dev" - source: hosted - version: "0.13.5" - http_multi_server: - dependency: transitive - description: - name: http_multi_server - sha256: "97486f20f9c2f7be8f514851703d0119c3596d14ea63227af6f7a481ef2b2f8b" - url: "https://pub.dev" - source: hosted - version: "3.2.1" - http_parser: - dependency: transitive - description: - name: http_parser - sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" - url: "https://pub.dev" - source: hosted - version: "4.0.2" - io: - dependency: transitive - description: - name: io - sha256: "0d4c73c3653ab85bf696d51a9657604c900a370549196a91f33e4c39af760852" - url: "https://pub.dev" - source: hosted - version: "1.0.3" - js: - dependency: transitive - description: - name: js - sha256: "323b7c70073cccf6b9b8d8b334be418a3293cfb612a560dc2737160a37bf61bd" - url: "https://pub.dev" - source: hosted - version: "0.6.6" - json_annotation: - dependency: transitive - description: - name: json_annotation - sha256: "3520fa844009431b5d4491a5a778603520cdc399ab3406332dcc50f93547258c" - url: "https://pub.dev" - source: hosted - version: "4.7.0" - logging: - dependency: transitive - description: - name: logging - sha256: c0bbfe94d46aedf9b8b3e695cf3bd48c8e14b35e3b2c639e0aa7755d589ba946 - url: "https://pub.dev" - source: hosted - version: "1.1.0" - matcher: - dependency: transitive - description: - name: matcher - sha256: c94db23593b89766cda57aab9ac311e3616cf87c6fa4e9749df032f66f30dcb8 - url: "https://pub.dev" - source: hosted - version: "0.12.14" - material_color_utilities: - dependency: transitive - description: - name: material_color_utilities - sha256: d92141dc6fe1dad30722f9aa826c7fbc896d021d792f80678280601aff8cf724 - url: "https://pub.dev" - source: hosted - version: "0.2.0" - meta: - dependency: transitive - description: - name: meta - sha256: "6c268b42ed578a53088d834796959e4a1814b5e9e164f147f580a386e5decf42" - url: "https://pub.dev" - source: hosted - version: "1.8.0" - mime: - dependency: transitive - description: - name: mime - sha256: e4ff8e8564c03f255408decd16e7899da1733852a9110a58fe6d1b817684a63e - url: "https://pub.dev" - source: hosted - version: "1.0.4" - nested: - dependency: transitive - description: - name: nested - sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20" - url: "https://pub.dev" - source: hosted - version: "1.0.0" - package_config: - dependency: transitive - description: - name: package_config - sha256: "1c5b77ccc91e4823a5af61ee74e6b972db1ef98c2ff5a18d3161c982a55448bd" - url: "https://pub.dev" - source: hosted - version: "2.1.0" - path: - dependency: transitive - description: - name: path - sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917" - url: "https://pub.dev" - source: hosted - version: "1.8.3" - path_provider_linux: - dependency: transitive - description: - name: path_provider_linux - sha256: ab0987bf95bc591da42dffb38c77398fc43309f0b9b894dcc5d6f40c4b26c379 - url: "https://pub.dev" - source: hosted - version: "2.1.7" - path_provider_platform_interface: - dependency: transitive - description: - name: path_provider_platform_interface - sha256: f0abc8ebd7253741f05488b4813d936b4d07c6bae3e86148a09e342ee4b08e76 - url: "https://pub.dev" - source: hosted - version: "2.0.5" - path_provider_windows: - dependency: transitive - description: - name: path_provider_windows - sha256: bcabbe399d4042b8ee687e17548d5d3f527255253b4a639f5f8d2094a9c2b45c - url: "https://pub.dev" - source: hosted - version: "2.1.3" - platform: - dependency: transitive - description: - name: platform - sha256: "4a451831508d7d6ca779f7ac6e212b4023dd5a7d08a27a63da33756410e32b76" - url: "https://pub.dev" - source: hosted - version: "3.1.0" - plugin_platform_interface: - dependency: transitive - description: - name: plugin_platform_interface - sha256: dbf0f707c78beedc9200146ad3cb0ab4d5da13c246336987be6940f026500d3a - url: "https://pub.dev" - source: hosted - version: "2.1.3" - pool: - dependency: transitive - description: - name: pool - sha256: "20fe868b6314b322ea036ba325e6fc0711a22948856475e2c2b6306e8ab39c2a" - url: "https://pub.dev" - source: hosted - version: "1.5.1" - process: - dependency: transitive - description: - name: process - sha256: "53fd8db9cec1d37b0574e12f07520d582019cb6c44abf5479a01505099a34a09" - url: "https://pub.dev" - source: hosted - version: "4.2.4" - provider: - dependency: "direct main" - description: - name: provider - sha256: cdbe7530b12ecd9eb455bdaa2fcb8d4dad22e80b8afb4798b41479d5ce26847f - url: "https://pub.dev" - source: hosted - version: "6.0.5" - pub_semver: - dependency: transitive - description: - name: pub_semver - sha256: "307de764d305289ff24ad257ad5c5793ce56d04947599ad68b3baa124105fc17" - url: "https://pub.dev" - source: hosted - version: "2.1.3" - pubspec_parse: - dependency: transitive - description: - name: pubspec_parse - sha256: "75f6614d6dde2dc68948dffbaa4fe5dae32cd700eb9fb763fe11dfb45a3c4d0a" - url: "https://pub.dev" - source: hosted - version: "1.2.1" - shared_preferences: - dependency: "direct main" - description: - name: shared_preferences - sha256: "95688ad7fc320f8566f28e2ee91b6743c10b433ccc262f6469f3007f2aa62e78" - url: "https://pub.dev" - source: hosted - version: "2.0.16" - shared_preferences_android: - dependency: transitive - description: - name: shared_preferences_android - sha256: "8e251f3c986002b65fed6396bce81f379fb63c27317d49743cf289fd0fd1ab97" - url: "https://pub.dev" - source: hosted - version: "2.0.14" - shared_preferences_foundation: - dependency: transitive - description: - name: shared_preferences_foundation - sha256: f451880807c86a6d4591642c8250ee17197cabef8536f072d3c44013dca44e04 - url: "https://pub.dev" - source: hosted - version: "2.1.1" - shared_preferences_linux: - dependency: transitive - description: - name: shared_preferences_linux - sha256: fbc3cd6826896b66a5f576b025e4f344f780c84ea7f8203097a353370607a2c8 - url: "https://pub.dev" - source: hosted - version: "2.1.2" - shared_preferences_platform_interface: - dependency: transitive - description: - name: shared_preferences_platform_interface - sha256: da9431745ede5ece47bc26d5d73a9d3c6936ef6945c101a5aca46f62e52c1cf3 - url: "https://pub.dev" - source: hosted - version: "2.1.0" - shared_preferences_web: - dependency: transitive - description: - name: shared_preferences_web - sha256: a4b5bc37fe1b368bbc81f953197d55e12f49d0296e7e412dfe2d2d77d6929958 - url: "https://pub.dev" - source: hosted - version: "2.0.4" - shared_preferences_windows: - dependency: transitive - description: - name: shared_preferences_windows - sha256: "07c274c2115d4d5e4280622abb09f0980e2c5b1fcdc98ae9f59a3bad5bfc1f26" - url: "https://pub.dev" - source: hosted - version: "2.1.2" - shelf: - dependency: transitive - description: - name: shelf - sha256: c24a96135a2ccd62c64b69315a14adc5c3419df63b4d7c05832a346fdb73682c - url: "https://pub.dev" - source: hosted - version: "1.4.0" - shelf_web_socket: - dependency: transitive - description: - name: shelf_web_socket - sha256: a988c0e8d8ffbdb8a28aa7ec8e449c260f3deb808781fe1284d22c5bba7156e8 - url: "https://pub.dev" - source: hosted - version: "1.0.3" - sky_engine: - dependency: transitive - description: flutter - source: sdk - version: "0.0.99" - source_gen: - dependency: transitive - description: - name: source_gen - sha256: "2d79738b6bbf38a43920e2b8d189e9a3ce6cc201f4b8fc76be5e4fe377b1c38d" - url: "https://pub.dev" - source: hosted - version: "1.2.6" - source_span: - dependency: transitive - description: - name: source_span - sha256: dd904f795d4b4f3b870833847c461801f6750a9fa8e61ea5ac53f9422b31f250 - url: "https://pub.dev" - source: hosted - version: "1.9.1" - stack_trace: - dependency: transitive - description: - name: stack_trace - sha256: c3c7d8edb15bee7f0f74debd4b9c5f3c2ea86766fe4178eb2a18eb30a0bdaed5 - url: "https://pub.dev" - source: hosted - version: "1.11.0" - stream_channel: - dependency: transitive - description: - name: stream_channel - sha256: "83615bee9045c1d322bbbd1ba209b7a749c2cbcdcb3fdd1df8eb488b3279c1c8" - url: "https://pub.dev" - source: hosted - version: "2.1.1" - stream_transform: - dependency: transitive - description: - name: stream_transform - sha256: "14a00e794c7c11aa145a170587321aedce29769c08d7f58b1d141da75e3b1c6f" - url: "https://pub.dev" - source: hosted - version: "2.1.0" - string_scanner: - dependency: transitive - description: - name: string_scanner - sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" - url: "https://pub.dev" - source: hosted - version: "1.2.0" - term_glyph: - dependency: transitive - description: - name: term_glyph - sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 - url: "https://pub.dev" - source: hosted - version: "1.2.1" - test_api: - dependency: transitive - description: - name: test_api - sha256: c9282698e2982b6c3817037554e52f99d4daba493e8028f8112a83d68ccd0b12 - url: "https://pub.dev" - source: hosted - version: "0.4.17" - timing: - dependency: transitive - description: - name: timing - sha256: c386d07d7f5efc613479a7c4d9d64b03710b03cfaa7e8ad5f2bfb295a1f0dfad - url: "https://pub.dev" - source: hosted - version: "1.0.0" - typed_data: - dependency: transitive - description: - name: typed_data - sha256: "26f87ade979c47a150c9eaab93ccd2bebe70a27dc0b4b29517f2904f04eb11a5" - url: "https://pub.dev" - source: hosted - version: "1.3.1" - vector_math: - dependency: transitive - description: - name: vector_math - sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" - url: "https://pub.dev" - source: hosted - version: "2.1.4" - watcher: - dependency: transitive - description: - name: watcher - sha256: "6a7f46926b01ce81bfc339da6a7f20afbe7733eff9846f6d6a5466aa4c6667c0" - url: "https://pub.dev" - source: hosted - version: "1.0.2" - web_socket_channel: - dependency: transitive - description: - name: web_socket_channel - sha256: ca49c0bc209c687b887f30527fb6a9d80040b072cc2990f34b9bec3e7663101b - url: "https://pub.dev" - source: hosted - version: "2.3.0" - win32: - dependency: transitive - description: - name: win32 - sha256: c9ebe7ee4ab0c2194e65d3a07d8c54c5d00bb001b76081c4a04cdb8448b59e46 - url: "https://pub.dev" - source: hosted - version: "3.1.3" - xdg_directories: - dependency: transitive - description: - name: xdg_directories - sha256: bd512f03919aac5f1313eb8249f223bacf4927031bf60b02601f81f687689e86 - url: "https://pub.dev" - source: hosted - version: "0.2.0+3" - yaml: - dependency: transitive - description: - name: yaml - sha256: "23812a9b125b48d4007117254bca50abb6c712352927eece9e155207b1db2370" - url: "https://pub.dev" - source: hosted - version: "3.1.1" -sdks: - dart: ">=2.19.0-345.0.dev <3.0.0-200.0.dev" - flutter: ">=3.0.0" diff --git a/packages/flutter_hooks/lib/src/animation.dart b/packages/flutter_hooks/lib/src/animation.dart index be066a9f..bce00cbe 100644 --- a/packages/flutter_hooks/lib/src/animation.dart +++ b/packages/flutter_hooks/lib/src/animation.dart @@ -10,8 +10,8 @@ T useAnimation(Animation animation) { return animation.value; } -class _UseAnimationHook extends _ListenableHook { - const _UseAnimationHook(Animation animation) : super(animation); +class _UseAnimationHook extends _ListenableHook { + const _UseAnimationHook(Animation animation) : super(animation); @override _UseAnimationStateHook createState() { diff --git a/packages/flutter_hooks/lib/src/framework.dart b/packages/flutter_hooks/lib/src/framework.dart index f1137f90..d46bcd8f 100644 --- a/packages/flutter_hooks/lib/src/framework.dart +++ b/packages/flutter_hooks/lib/src/framework.dart @@ -153,7 +153,7 @@ Calling them outside of build method leads to an unstable state and is therefore /// /// - `hook1.keys == hook2.keys` (typically if the list is immutable) /// - If there's any difference in the content of [Hook.keys], using `operator==`. - static bool shouldPreserveState(Hook hook1, Hook hook2) { + static bool shouldPreserveState(Hook hook1, Hook hook2) { final p1 = hook1.keys; final p2 = hook2.keys; @@ -350,10 +350,10 @@ extension on HookElement { mixin HookElement on ComponentElement { static HookElement? _currentHookElement; - _Entry? _currentHookState; - final _hooks = LinkedList<_Entry>(); + _Entry>>? _currentHookState; + final _hooks = LinkedList<_Entry>>>(); final _shouldRebuildQueue = LinkedList<_Entry>(); - LinkedList<_Entry>? _needDispose; + LinkedList<_Entry>>>? _needDispose; bool? _isOptionalRebuild = false; Widget? _buildCache; @@ -363,7 +363,7 @@ mixin HookElement on ComponentElement { /// A read-only list of all available hooks. /// /// In release mode, returns `null`. - List? get debugHooks { + List>>? get debugHooks { if (!kDebugMode) { return null; } diff --git a/packages/flutter_hooks/lib/src/keep_alive.dart b/packages/flutter_hooks/lib/src/keep_alive.dart index 5f915b25..f7c5fba1 100644 --- a/packages/flutter_hooks/lib/src/keep_alive.dart +++ b/packages/flutter_hooks/lib/src/keep_alive.dart @@ -34,7 +34,7 @@ class _AutomaticKeepAliveHookState } void _releaseKeepAlive() { - _keepAliveHandle?.release(); + _keepAliveHandle?.dispose(); _keepAliveHandle = null; } diff --git a/packages/flutter_hooks/lib/src/listenable.dart b/packages/flutter_hooks/lib/src/listenable.dart index 8b4c214f..1284f2dc 100644 --- a/packages/flutter_hooks/lib/src/listenable.dart +++ b/packages/flutter_hooks/lib/src/listenable.dart @@ -11,7 +11,8 @@ T useValueListenable(ValueListenable valueListenable) { } class _UseValueListenableHook extends _ListenableHook { - const _UseValueListenableHook(ValueListenable animation) : super(animation); + const _UseValueListenableHook(ValueListenable animation) + : super(animation); @override _UseValueListenableStateHook createState() { diff --git a/packages/flutter_hooks/test/hook_widget_test.dart b/packages/flutter_hooks/test/hook_widget_test.dart index 15b7a5be..f0365d6e 100644 --- a/packages/flutter_hooks/test/hook_widget_test.dart +++ b/packages/flutter_hooks/test/hook_widget_test.dart @@ -853,7 +853,7 @@ void main() { testWidgets('hot-reload can add hooks at the end of the list', (tester) async { - late HookTest hook1; + late HookTest hook1; final dispose2 = MockDispose(); final initHook2 = MockInitHook(); @@ -1011,7 +1011,7 @@ void main() { verifyZeroInteractions(didUpdateHook2); }); testWidgets('hot-reload disposes hooks when type change', (tester) async { - late HookTest hook1; + late HookTest hook1; final dispose2 = MockDispose(); final initHook2 = MockInitHook(); @@ -1113,7 +1113,7 @@ void main() { }); testWidgets('hot-reload disposes hooks when type change', (tester) async { - late HookTest hook1; + late HookTest hook1; final dispose2 = MockDispose(); final initHook2 = MockInitHook(); diff --git a/packages/flutter_hooks/test/mock.dart b/packages/flutter_hooks/test/mock.dart index d2fee245..f7aaab8a 100644 --- a/packages/flutter_hooks/test/mock.dart +++ b/packages/flutter_hooks/test/mock.dart @@ -110,7 +110,8 @@ class MockInitHook extends Mock { void call(); } -class MockCreateState> extends Mock { +class MockCreateState>> + extends Mock { MockCreateState(this.value); final T value; @@ -152,7 +153,7 @@ class MockReassemble extends Mock { } class MockDidUpdateHook extends Mock { - void call(HookTest? hook); + void call(HookTest? hook); } class MockDispose extends Mock { diff --git a/packages/flutter_hooks/test/use_focus_node_test.dart b/packages/flutter_hooks/test/use_focus_node_test.dart index 8e06beb4..91642c66 100644 --- a/packages/flutter_hooks/test/use_focus_node_test.dart +++ b/packages/flutter_hooks/test/use_focus_node_test.dart @@ -28,14 +28,13 @@ void main() { ); expect(previousValue, focusNode); - // ignore: invalid_use_of_protected_member - expect(focusNode.hasListeners, isFalse); + // check you can add listener (only possible if not disposed) + focusNode.addListener(() {}); await tester.pumpWidget(Container()); expect( - // ignore: invalid_use_of_protected_member - () => focusNode.hasListeners, + () => focusNode.addListener(() {}), throwsAssertionError, ); }); diff --git a/packages/flutter_hooks/test/use_state_test.dart b/packages/flutter_hooks/test/use_state_test.dart index e38b6bac..7817ec93 100644 --- a/packages/flutter_hooks/test/use_state_test.dart +++ b/packages/flutter_hooks/test/use_state_test.dart @@ -35,8 +35,7 @@ void main() { // dispose await tester.pumpWidget(const SizedBox()); - // ignore: invalid_use_of_protected_member - expect(() => state.hasListeners, throwsFlutterError); + expect(() => state.addListener(() {}), throwsFlutterError); }); testWidgets('no initial data', (tester) async { @@ -69,8 +68,7 @@ void main() { // dispose await tester.pumpWidget(const SizedBox()); - // ignore: invalid_use_of_protected_member - expect(() => state.hasListeners, throwsFlutterError); + expect(() => state.addListener(() {}), throwsFlutterError); }); testWidgets('debugFillProperties should print state hook ', (tester) async { diff --git a/packages/flutter_hooks/test/use_stream_controller_test.dart b/packages/flutter_hooks/test/use_stream_controller_test.dart index 0841d123..ede731eb 100644 --- a/packages/flutter_hooks/test/use_stream_controller_test.dart +++ b/packages/flutter_hooks/test/use_stream_controller_test.dart @@ -58,7 +58,10 @@ void main() { return Container(); })); - expect(controller, isNot(isInstanceOf())); + expect( + controller, + isNot(isInstanceOf>()), + ); expect(controller.onListen, isNull); expect(controller.onCancel, isNull); expect(() => controller.onPause, throwsUnsupportedError); @@ -77,7 +80,10 @@ void main() { })); expect(controller, previousController); - expect(controller, isNot(isInstanceOf())); + expect( + controller, + isNot(isInstanceOf>()), + ); expect(controller.onListen, onListen); expect(controller.onCancel, onCancel); expect(() => controller.onPause, throwsUnsupportedError); @@ -95,7 +101,7 @@ void main() { return Container(); })); - expect(controller, isInstanceOf()); + expect(controller, isInstanceOf>()); expect(controller.onListen, isNull); expect(controller.onCancel, isNull); expect(() => controller.onPause, throwsUnsupportedError); @@ -113,7 +119,7 @@ void main() { })); expect(controller, previousController); - expect(controller, isInstanceOf()); + expect(controller, isInstanceOf>()); expect(controller.onListen, onListen); expect(controller.onCancel, onCancel); expect(() => controller.onPause, throwsUnsupportedError); diff --git a/packages/flutter_hooks/test/use_value_notifier_test.dart b/packages/flutter_hooks/test/use_value_notifier_test.dart index 26c0f4d7..5e8ba7d9 100644 --- a/packages/flutter_hooks/test/use_value_notifier_test.dart +++ b/packages/flutter_hooks/test/use_value_notifier_test.dart @@ -66,8 +66,7 @@ void main() { // dispose await tester.pumpWidget(const SizedBox()); - // ignore: invalid_use_of_protected_member - expect(() => state.hasListeners, throwsFlutterError); + expect(() => state.addListener(() {}), throwsFlutterError); }); testWidgets('no initial data', (tester) async { @@ -108,8 +107,7 @@ void main() { // dispose await tester.pumpWidget(const SizedBox()); - // ignore: invalid_use_of_protected_member - expect(() => state.hasListeners, throwsFlutterError); + expect(() => state.addListener(() {}), throwsFlutterError); }); testWidgets('creates new valuenotifier when key change', (tester) async { From f4ec3ca5dbc0fba094191d83cfc8d59803295544 Mon Sep 17 00:00:00 2001 From: sejun Date: Sun, 19 Feb 2023 23:37:07 +0900 Subject: [PATCH 279/384] ko_kr translate (#342) --- README.md | 2 +- .../resources/translations/ko_kr/README.md | 373 ++++++++++++++++++ .../resources/translations/pt_br/README.md | 2 +- 3 files changed, 375 insertions(+), 2 deletions(-) create mode 100644 packages/flutter_hooks/resources/translations/ko_kr/README.md diff --git a/README.md b/README.md index ad69f566..d69d514f 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -[English](https://github.com/rrousselGit/flutter_hooks/blob/master/README.md) | [Português](https://github.com/rrousselGit/flutter_hooks/blob/master/packages/flutter_hooks/resources/translations/pt_br/README.md) +[English](https://github.com/rrousselGit/flutter_hooks/blob/master/README.md) | [Português](https://github.com/rrousselGit/flutter_hooks/blob/master/packages/flutter_hooks/resources/translations/pt_br/README.md) | [한국어](https://github.com/rrousselGit/flutter_hooks/blob/master/packages/flutter_hooks/resources/translations/ko_kr/README.md) [![Build](https://github.com/rrousselGit/flutter_hooks/workflows/Build/badge.svg)](https://github.com/rrousselGit/flutter_hooks/actions?query=workflow%3ABuild) [![codecov](https://codecov.io/gh/rrousselGit/flutter_hooks/branch/master/graph/badge.svg)](https://codecov.io/gh/rrousselGit/flutter_hooks) [![pub package](https://img.shields.io/pub/v/flutter_hooks.svg)](https://pub.dartlang.org/packages/flutter_hooks) [![pub package](https://img.shields.io/badge/Awesome-Flutter-blue.svg?longCache=true&style=flat-square)](https://github.com/Solido/awesome-flutter) Discord diff --git a/packages/flutter_hooks/resources/translations/ko_kr/README.md b/packages/flutter_hooks/resources/translations/ko_kr/README.md new file mode 100644 index 00000000..8bc851ee --- /dev/null +++ b/packages/flutter_hooks/resources/translations/ko_kr/README.md @@ -0,0 +1,373 @@ +[English](https://github.com/rrousselGit/flutter_hooks/blob/master/README.md) | [Português](https://github.com/rrousselGit/flutter_hooks/blob/master/packages/flutter_hooks/resources/translations/pt_br/README.md) | [한국어](https://github.com/rrousselGit/flutter_hooks/blob/master/packages/flutter_hooks/resources/translations/ko_kr/README.md) + +[![Build](https://github.com/rrousselGit/flutter_hooks/workflows/Build/badge.svg)](https://github.com/rrousselGit/flutter_hooks/actions?query=workflow%3ABuild) [![codecov](https://codecov.io/gh/rrousselGit/flutter_hooks/branch/master/graph/badge.svg)](https://codecov.io/gh/rrousselGit/flutter_hooks) [![pub package](https://img.shields.io/pub/v/flutter_hooks.svg)](https://pub.dartlang.org/packages/flutter_hooks) [![pub package](https://img.shields.io/badge/Awesome-Flutter-blue.svg?longCache=true&style=flat-square)](https://github.com/Solido/awesome-flutter) +Discord + + + +# Flutter Hooks + +A Flutter implementation of React hooks: https://medium.com/@dan_abramov/making-sense-of-react-hooks-fdbde8803889 + +훅은 `widget`의 라이프사이클을 관리하는 새로운 종류의 객체입니다. +훅은 한가지 이유로 존재합니다: 중복을 제거함으로써 위젯사이에 코드 공유를 증가시킵니다. + +## Motivation + +`StatefulWidget`는 아래와 같은 문제점이 있습니다: `initState`나 `dispose`와 같은 로직을 재사용하기가 매우 어렵습니다. 한가지 분명한 예시는 `AnimationController`입니다: + +```dart +class Example extends StatefulWidget { + final Duration duration; + + const Example({Key? key, required this.duration}) + : super(key: key); + + @override + _ExampleState createState() => _ExampleState(); +} + +class _ExampleState extends State with SingleTickerProviderStateMixin { + AnimationController? _controller; + + @override + void initState() { + super.initState(); + _controller = AnimationController(vsync: this, duration: widget.duration); + } + + @override + void didUpdateWidget(Example oldWidget) { + super.didUpdateWidget(oldWidget); + if (widget.duration != oldWidget.duration) { + _controller!.duration = widget.duration; + } + } + + @override + void dispose() { + _controller!.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Container(); + } +} +``` + +`AnimationController` 를 사용하는 모든 위젯은 이 로직을 재사용하기 위해 모두 이 로직을 재구현해야 합니다. 이는 물론 원치 않는 결과입니다. + +Dart mixins 는 이 문제를 해결할 수 있지만, 다른 문제점들이 있습니다: +- 주어진 mixin 은 한 클래스당 한번만 사용할 수 있습니다. +- mixin 과 클래스는 같은 객체를 공유합니다.\ + 이는 mixin 이 같은 이름의 변수를 정의하면, 컴파일 에러에서부터 알 수 없는 결과까지 다양한 결과를 가져올 수 있음을 의미합니다. + +--- + +이 라이브러리는 세번째 해결책을 제안합니다: + +```dart +class Example extends HookWidget { + const Example({Key? key, required this.duration}) + : super(key: key); + + final Duration duration; + + @override + Widget build(BuildContext context) { + final controller = useAnimationController(duration: duration); + return Container(); + } +} +``` +이 코드는 이전 예제와 기능적으로 동일합니다. 여전히 `AnimationController` 를 dispose 하고, `Example.duration` 이 변경될 때 `duration` 을 업데이트합니다. +하지만 당신은 아마도 다음과 같은 생각을 하고 있을 것입니다: + +> 모든 로직은 어디로 갔지? + +이 로직은 `useAnimationController` 함수에 있습니다. 이 함수는 이 라이브러리에 포함되어 있습니다. +(see [Existing hooks](https://github.com/rrousselGit/flutter_hooks#existing-hooks)) - 이것이 우리가 훅이라고 부르는 것 입니다. + +훅은 몇가지 사양(Sepcification)을 가지고 있는 새로운 종류의 객체입니다. + +- 훅은 `Hooks` 를 mix-in 한 위젯의 `build` 메소드에서만 사용할 수 있습니다. +- 동일한 훅은 임의의 수만큼 재사용될 수 있습니다. + 아래 코드는 두개의 독립적인 `AnimationController` 를 정의합니다. 그리고 위젯이 리빌드 될 때 이것들이 올바르게 보존됩니다. + + ```dart + Widget build(BuildContext context) { + final controller = useAnimationController(); + final controller2 = useAnimationController(); + return Container(); + } + ``` +- 훅은 서로와 위젯에 완전히 독립적입니다. + 이것은 훅을 패키지로 추출하고 [pub](https://pub.dartlang.org/) 에서 다른 사람들이 사용할 수 있도록 쉽게 만들어 줍니다. + +## Principle + +`State`와 비슷하게 훅은 `Widget`의 `Element`에 저장됩니다. 그러나 `State` 하나만 갖는 것 대신에, `Element`는 `List`를 갖습니다. 그리고 훅을 사용하기 위해서는 `Hook.use`를 호출해야 합니다. + +`use` 함수에 의해 반환된 훅은 `use`가 호출된 횟수에 기반합니다. +첫번째 호출은 첫번째 훅을 반환하고, 두번째 호출은 두번째 훅을 반환하고, 세번째 호출은 세번째 훅을 반환하며 이런식으로 진행됩니다. + +만약 이 아이디어가 아직도 이해가 안된다면, 아래와 같이 훅을 구현하는 것이 가능합니다: + + +```dart +class HookElement extends Element { + List _hooks; + int _hookIndex; + + T use(Hook hook) => _hooks[_hookIndex++].build(this); + + @override + performRebuild() { + _hookIndex = 0; + super.performRebuild(); + } +} +``` + +훅을 구현하는 더 다양한 예시를 보기위해 React 에서 훅이 어떻게 구현되어 있는지 훌륭한 글이 있습니다: https://medium.com/@ryardley/react-hooks-not-magic-just-arrays-cd4f1857236e + +## Rules + +훅이 인덱스로부터 얻어지기 때문에, 몇가지 규칙을 지켜야 합니다: + +### `use` 로 시작하는 이름을 사용하세요: + +```dart +Widget build(BuildContext context) { + // starts with `use`, good name + useMyHook(); + // doesn't start with `use`, could confuse people into thinking that this isn't a hook + myHook(); + // .... +} +``` + +### 훅을 조건 없이 호출하세요 + +```dart +Widget build(BuildContext context) { + useMyHook(); + // .... +} +``` + +### `use`를 조건문 안에 넣지 마세요 + +```dart +Widget build(BuildContext context) { + if (condition) { + useMyHook(); + } + // .... +} +``` + +--- + +### About hot-reload + +훅은 인덱스로부터 얻어지기 때문에, 리팩토링을 하면서 핫 리로드가 어플리케이션을 깨뜨릴 수 있을 것 같다고 생각할 수 있습니다. + +하지만 걱정하지 마세요, `HookWidget` 은 기본 핫 리로드 동작을 훅과 함께 작동하도록 재정의합니다. 그래도, 훅의 상태가 리셋될 수 있는 상황이 있습니다. + +아래의 훅 리스트를 생각해보세요: + + +```dart +useA(); +useB(0); +useC(); +``` + +그런다음 hot-reload를 수행한 뒤 `HookB` 의 파라미터를 편집했다고 가정해봅시다: + + +```dart +useA(); +useB(42); +useC(); +``` + +모든 훅이 잘 작동하고, 모든 훅의 상태가 유지됩니다. + +이제 `HookB`가 제거되었다고 생각해 봅시다. 우리는 이제 다음과 같은것을 가지게 됩니다: + + +```dart +useA(); +useC(); +``` + +이 상황에서 `HookA` 는 상태를 유지하지만 `HookC` 는 리셋됩니다. +이것은 리팩토링 후 핫 리로드를 수행하면, 첫번째로 영향을 받은 행 이후의 모든 훅이 제거되기 때문에 발생합니다. +그래서 `HookC`가 `HookB` 뒤에 있기 때문에 제거됩니다. + +## How to create a hook + +훅을 생성하기위한 두가지 방법이 있습니다: + +- 함수 + 함수는 훅을 작성하는 가장 일반적인 방법입니다. 훅이 자연스럽게 합성 가능하기 때문에, 함수는 다른 훅을 결합하여 더 복잡한 커스텀 훅을 만들 수 있습니다. 관례상, 이러한 함수는 `use`로 시작됩니다. + + 아래의 코드는 변수를 생성하고, 값이 변경될 때마다 콘솔에 로그를 남기는 커스텀 훅을 정의합니다: + + ```dart + ValueNotifier useLoggedState([T initialData]) { + final result = useState(initialData); + useValueChanged(result.value, (_, __) { + print(result.value); + }); + return result; + } + ``` + +- 클래스 + + 훅이 너무 복잡해지면, `Hook` 을 확장하는 클래스로 변환할 수 있습니다. 이 클래스는 `Hook.use`를 사용하여 사용할 수 있습니다. + 클래스로 훅을 정의하면, 훅은 `State` 클래스와 매우 유사하게 보일 것이며 `initHook`, `dispose` 및 `setState`와 같은 위젯의 라이프 사이클 및 메서드에 액세스 할 수 있습니다. + + 이와같이 함수 내에 클래스를 숨기는것은 좋은 예시입니다: + + ```dart + Result useMyHook() { + return use(const _TimeAlive()); + } + ``` + + 아래의 코드는 `State`가 얼마나 오래 살아있었는지 콘솔에 출력하는 훅을 정의합니다: + + ```dart + class _TimeAlive extends Hook { + const _TimeAlive(); + + @override + _TimeAliveState createState() => _TimeAliveState(); + } + + class _TimeAliveState extends HookState { + DateTime start; + + @override + void initHook() { + super.initHook(); + start = DateTime.now(); + } + + @override + void build(BuildContext context) {} + + @override + void dispose() { + print(DateTime.now().difference(start)); + super.dispose(); + } + } + ``` + +## Existing hooks + +Flutter_Hooks 는 이미 재사용 가능한 훅 목록을 제공합니다. 이 목록은 다음과 같이 구분됩니다: + +### Primitives + +다른 위젯의 라이프사이클과 상호작용하는 low-level 의 훅 입니다. + +| Name | Description | +| ----------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------- | +| [useEffect](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useEffect.html) | Useful for side-effects and optionally canceling them. | +| [useState](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useState.html) | Creates a variable and subscribes to it. | +| [useMemoized](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useMemoized.html) | Caches the instance of a complex object. | +| [useRef](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useRef.html) | Creates an object that contains a single mutable property. | +| [useCallback](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useCallback.html) | Caches a function instance. | +| [useContext](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useContext.html) | Obtains the `BuildContext` of the building `HookWidget`. | +| [useValueChanged](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useValueChanged.html) | Watches a value and triggers a callback whenever its value changed. | + +### Object-binding + +이 카테고리의 훅은 기존의 Flutter/Dart 객체를 조작합니다. +이 훅은 객체를 생성/업데이트/삭제하는 역할을 합니다. + +#### dart:async related hooks: + +| Name | Description | +| ------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------- | +| [useStream](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useStream.html) | Subscribes to a `Stream` and returns its current state as an `AsyncSnapshot`. | +| [useStreamController](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useStreamController.html) | Creates a `StreamController` which will automatically be disposed. | +| [useFuture](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useFuture.html) | Subscribes to a `Future` and returns its current state as an `AsyncSnapshot`. | + +#### Animation related hooks: + +| Name | Description | +| --------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------- | +| [useSingleTickerProvider](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useSingleTickerProvider.html) | Creates a single usage `TickerProvider`. | +| [useAnimationController](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useAnimationController.html) | Creates an `AnimationController` which will be automatically disposed. | +| [useAnimation](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useAnimation.html) | Subscribes to an `Animation` and returns its value. | + +#### Listenable related hooks: + +| Name | Description | +| ----------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------- | +| [useListenable](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useListenable.html) | Subscribes to a `Listenable` and marks the widget as needing build whenever the listener is called. | +| [useListenableSelector](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useListenableSelector.html) | Similar to `useListenable`, but allows filtering UI rebuilds | +| [useValueNotifier](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useValueNotifier.html) | Creates a `ValueNotifier` which will be automatically disposed. | +| [useValueListenable](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useValueListenable.html) | Subscribes to a `ValueListenable` and return its value. | + +#### Misc hooks: + +특정한 theme을 가지지 않는 일련의 훅 입니다. + +| Name | Description | +| ------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------- | +| [useReducer](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useReducer.html) | An alternative to `useState` for more complex states. | +| [usePrevious](https://pub.dartlang.org/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.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useFocusNode.html) | Creates a `FocusNode`. | +| [useTabController](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useTabController.html) | Creates and disposes a `TabController`. | +| [useScrollController](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useScrollController.html) | Creates and disposes a `ScrollController`. | +| [usePageController](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/usePageController.html) | Creates and disposes a `PageController`. | +| [useAppLifecycleState](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useAppLifecycleState.html) | Returns the current `AppLifecycleState` and rebuilds the widget on change. | +| [useOnAppLifecycleStateChange](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useOnAppLifecycleStateChange.html) | Listens to `AppLifecycleState` changes and triggers a callback on change. | +| [useTransformationController](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useTransformationController.html) | Creates and disposes a `TransformationController`. | +| [useIsMounted](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useIsMounted.html) | An equivalent to `State.mounted` for hooks. | +| [useAutomaticKeepAlive](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useAutomaticKeepAlive.html) | An equivalent to the `AutomaticKeepAlive` widget for hooks. | +| [useOnPlatformBrightnessChange](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useOnPlatformBrightnessChange.html) | Listens to platform `Brightness` changes and triggers a callback on change.| + +## Contributions + +Contributions are welcomed! + +If you feel that a hook is missing, feel free to open a pull-request. + +For a custom-hook to be merged, you will need to do the following: + +- Describe the use-case. + + Open an issue explaining why we need this hook, how to use it, ... + This is important as a hook will not get merged if the hook doesn't appeal to + a large number of people. + + If your hook is rejected, don't worry! A rejection doesn't mean that it won't + be merged later in the future if more people show interest in it. + In the mean-time, feel free to publish your hook as a package on https://pub.dev. + +- Write tests for your hook + + A hook will not be merged unless fully tested to avoid inadvertently breaking it + in the future. + +- Add it to the README and write documentation for it. + +## Sponsors + +

+ + + +

diff --git a/packages/flutter_hooks/resources/translations/pt_br/README.md b/packages/flutter_hooks/resources/translations/pt_br/README.md index 556dd62b..65f68158 100644 --- a/packages/flutter_hooks/resources/translations/pt_br/README.md +++ b/packages/flutter_hooks/resources/translations/pt_br/README.md @@ -1,4 +1,4 @@ -[English](https://github.com/rrousselGit/flutter_hooks/blob/master/README.md) | [Português](https://github.com/rrousselGit/flutter_hooks/blob/master/packages/flutter_hooks/resources/translations/pt_br/README.md) +[English](https://github.com/rrousselGit/flutter_hooks/blob/master/README.md) | [Português](https://github.com/rrousselGit/flutter_hooks/blob/master/packages/flutter_hooks/resources/translations/pt_br/README.md) | [한국어](https://github.com/rrousselGit/flutter_hooks/blob/master/packages/flutter_hooks/resources/translations/ko_kr/README.md) [![Build Status](https://travis-ci.org/rrousselGit/flutter_hooks.svg?branch=master)](https://travis-ci.org/rrousselGit/flutter_hooks) [![codecov](https://codecov.io/gh/rrousselGit/flutter_hooks/branch/master/graph/badge.svg)](https://codecov.io/gh/rrousselGit/flutter_hooks) [![pub package](https://img.shields.io/pub/v/flutter_hooks.svg)](https://pub.dartlang.org/packages/flutter_hooks) [![pub package](https://img.shields.io/badge/Awesome-Flutter-blue.svg?longCache=true&style=flat-square)](https://github.com/Solido/awesome-flutter) From a2c2df2f1690585f695227da68d0dd8cf48ef524 Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Sun, 19 Feb 2023 15:37:42 +0100 Subject: [PATCH 280/384] Update CHANGELOG.md --- packages/flutter_hooks/CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/flutter_hooks/CHANGELOG.md b/packages/flutter_hooks/CHANGELOG.md index 848c1c6a..71b458c4 100644 --- a/packages/flutter_hooks/CHANGELOG.md +++ b/packages/flutter_hooks/CHANGELOG.md @@ -1,3 +1,7 @@ +## Unreleased docs + +Added korean translation thanks to @sejun2 + ## 0.18.5+1 Update links to the repository From 56583fb2267582b8dd024ef74c513347c6ff95ce Mon Sep 17 00:00:00 2001 From: Sahil Sonawane Date: Sun, 19 Feb 2023 20:54:51 +0530 Subject: [PATCH 281/384] add `useFocusScopeNode` hook (#317) --- packages/flutter_hooks/CHANGELOG.md | 6 +- .../lib/src/{focus.dart => focus_node.dart} | 0 .../lib/src/focus_scope_node.dart | 74 +++++++++ packages/flutter_hooks/lib/src/hooks.dart | 3 +- .../test/use_focus_scope_node_test.dart | 153 ++++++++++++++++++ 5 files changed, 232 insertions(+), 4 deletions(-) rename packages/flutter_hooks/lib/src/{focus.dart => focus_node.dart} (100%) create mode 100644 packages/flutter_hooks/lib/src/focus_scope_node.dart create mode 100644 packages/flutter_hooks/test/use_focus_scope_node_test.dart diff --git a/packages/flutter_hooks/CHANGELOG.md b/packages/flutter_hooks/CHANGELOG.md index 71b458c4..bccbfc0a 100644 --- a/packages/flutter_hooks/CHANGELOG.md +++ b/packages/flutter_hooks/CHANGELOG.md @@ -1,7 +1,7 @@ -## Unreleased docs - -Added korean translation thanks to @sejun2 +## Unreleased minor +- Added korean translation (thanks to @sejun2) +- Add `useFocusScopeNode` hook (thanks to @iamsahilsonawane) ## 0.18.5+1 Update links to the repository diff --git a/packages/flutter_hooks/lib/src/focus.dart b/packages/flutter_hooks/lib/src/focus_node.dart similarity index 100% rename from packages/flutter_hooks/lib/src/focus.dart rename to packages/flutter_hooks/lib/src/focus_node.dart diff --git a/packages/flutter_hooks/lib/src/focus_scope_node.dart b/packages/flutter_hooks/lib/src/focus_scope_node.dart new file mode 100644 index 00000000..297a1bb0 --- /dev/null +++ b/packages/flutter_hooks/lib/src/focus_scope_node.dart @@ -0,0 +1,74 @@ +part of 'hooks.dart'; + +/// Creates an automatically disposed [FocusScopeNode]. +/// +/// See also: +/// - [FocusScopeNode] +FocusScopeNode useFocusScopeNode({ + String? debugLabel, + FocusOnKeyCallback? onKey, + FocusOnKeyEventCallback? onKeyEvent, + bool skipTraversal = false, + bool canRequestFocus = true, +}) { + return use( + _FocusScopeNodeHook( + debugLabel: debugLabel, + onKey: onKey, + onKeyEvent: onKeyEvent, + skipTraversal: skipTraversal, + canRequestFocus: canRequestFocus, + ), + ); +} + +class _FocusScopeNodeHook extends Hook { + const _FocusScopeNodeHook({ + this.debugLabel, + this.onKey, + this.onKeyEvent, + required this.skipTraversal, + required this.canRequestFocus, + }); + + final String? debugLabel; + final FocusOnKeyCallback? onKey; + final FocusOnKeyEventCallback? onKeyEvent; + final bool skipTraversal; + final bool canRequestFocus; + + @override + _FocusScopeNodeHookState createState() { + return _FocusScopeNodeHookState(); + } +} + +class _FocusScopeNodeHookState + extends HookState { + late final FocusScopeNode _focusScopeNode = FocusScopeNode( + debugLabel: hook.debugLabel, + onKey: hook.onKey, + onKeyEvent: hook.onKeyEvent, + skipTraversal: hook.skipTraversal, + canRequestFocus: hook.canRequestFocus, + ); + + @override + void didUpdateHook(_FocusScopeNodeHook oldHook) { + _focusScopeNode + ..debugLabel = hook.debugLabel + ..skipTraversal = hook.skipTraversal + ..canRequestFocus = hook.canRequestFocus + ..onKey = hook.onKey + ..onKeyEvent = hook.onKeyEvent; + } + + @override + FocusScopeNode build(BuildContext context) => _focusScopeNode; + + @override + void dispose() => _focusScopeNode.dispose(); + + @override + String get debugLabel => 'useFocusScopeNode'; +} diff --git a/packages/flutter_hooks/lib/src/hooks.dart b/packages/flutter_hooks/lib/src/hooks.dart index b9152b31..8b8cb7c4 100644 --- a/packages/flutter_hooks/lib/src/hooks.dart +++ b/packages/flutter_hooks/lib/src/hooks.dart @@ -14,7 +14,8 @@ part 'misc.dart'; part 'primitives.dart'; part 'tab_controller.dart'; part 'text_controller.dart'; -part 'focus.dart'; +part 'focus_node.dart'; +part 'focus_scope_node.dart'; part 'scroll_controller.dart'; part 'page_controller.dart'; part 'widgets_binding_observer.dart'; diff --git a/packages/flutter_hooks/test/use_focus_scope_node_test.dart b/packages/flutter_hooks/test/use_focus_scope_node_test.dart new file mode 100644 index 00000000..587b2350 --- /dev/null +++ b/packages/flutter_hooks/test/use_focus_scope_node_test.dart @@ -0,0 +1,153 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; + +import 'mock.dart'; + +void main() { + testWidgets('creates a focus scope node and disposes it', (tester) async { + late FocusScopeNode focusScopeNode; + await tester.pumpWidget( + HookBuilder(builder: (_) { + focusScopeNode = useFocusScopeNode(); + return Container(); + }), + ); + + expect(focusScopeNode, isA()); + // ignore: invalid_use_of_protected_member + expect(focusScopeNode.hasListeners, isFalse); + + final previousValue = focusScopeNode; + + await tester.pumpWidget( + HookBuilder(builder: (_) { + focusScopeNode = useFocusScopeNode(); + return Container(); + }), + ); + + expect(previousValue, focusScopeNode); + // ignore: invalid_use_of_protected_member + expect(focusScopeNode.hasListeners, isFalse); + + await tester.pumpWidget(Container()); + + expect( + () => focusScopeNode.dispose(), + throwsAssertionError, + ); + }); + + testWidgets('debugFillProperties', (tester) async { + await tester.pumpWidget( + HookBuilder(builder: (context) { + useFocusScopeNode(); + return const SizedBox(); + }), + ); + + final element = tester.element(find.byType(HookBuilder)); + + expect( + element + .toDiagnosticsNode(style: DiagnosticsTreeStyle.offstage) + .toStringDeep(), + equalsIgnoringHashCodes( + 'HookBuilder\n' + ' │ useFocusScopeNode: FocusScopeNode#00000\n' + ' └SizedBox(renderObject: RenderConstrainedBox#00000)\n', + ), + ); + }); + + testWidgets('default values matches with FocusScopeNode', (tester) async { + final official = FocusScopeNode(); + + late FocusScopeNode focusScopeNode; + await tester.pumpWidget( + HookBuilder(builder: (_) { + focusScopeNode = useFocusScopeNode(); + return Container(); + }), + ); + + expect(focusScopeNode.debugLabel, official.debugLabel); + expect(focusScopeNode.onKey, official.onKey); + expect(focusScopeNode.skipTraversal, official.skipTraversal); + expect(focusScopeNode.canRequestFocus, official.canRequestFocus); + }); + + testWidgets('has all the FocusScopeNode parameters', (tester) async { + KeyEventResult onKey(FocusNode node, RawKeyEvent event) => + KeyEventResult.ignored; + + KeyEventResult onKeyEvent(FocusNode node, KeyEvent event) => + KeyEventResult.ignored; + + late FocusScopeNode focusScopeNode; + await tester.pumpWidget( + HookBuilder(builder: (_) { + focusScopeNode = useFocusScopeNode( + debugLabel: 'Foo', + onKey: onKey, + onKeyEvent: onKeyEvent, + skipTraversal: true, + canRequestFocus: false, + ); + return Container(); + }), + ); + + expect(focusScopeNode.debugLabel, 'Foo'); + expect(focusScopeNode.onKey, onKey); + expect(focusScopeNode.onKeyEvent, onKeyEvent); + expect(focusScopeNode.skipTraversal, true); + expect(focusScopeNode.canRequestFocus, false); + }); + + testWidgets('handles parameter change', (tester) async { + KeyEventResult onKey(FocusNode node, RawKeyEvent event) => + KeyEventResult.ignored; + KeyEventResult onKey2(FocusNode node, RawKeyEvent event) => + KeyEventResult.ignored; + + KeyEventResult onKeyEvent(FocusNode node, KeyEvent event) => + KeyEventResult.ignored; + KeyEventResult onKeyEvent2(FocusNode node, KeyEvent event) => + KeyEventResult.ignored; + + late FocusScopeNode focusScopeNode; + await tester.pumpWidget( + HookBuilder(builder: (_) { + focusScopeNode = useFocusScopeNode( + debugLabel: 'Foo', + onKey: onKey, + onKeyEvent: onKeyEvent, + skipTraversal: true, + canRequestFocus: false, + ); + + return Container(); + }), + ); + + await tester.pumpWidget( + HookBuilder(builder: (_) { + focusScopeNode = useFocusScopeNode( + debugLabel: 'Bar', + onKey: onKey2, + onKeyEvent: onKeyEvent2, + ); + + return Container(); + }), + ); + + expect(focusScopeNode.onKey, onKey2); + expect(focusScopeNode.onKeyEvent, onKeyEvent2); + expect(focusScopeNode.debugLabel, 'Bar'); + expect(focusScopeNode.skipTraversal, false); + expect(focusScopeNode.canRequestFocus, true); + }); +} From e67ea428ab39594cff934235fe50d1acf8f6be34 Mon Sep 17 00:00:00 2001 From: Russell Power Date: Sun, 19 Feb 2023 07:25:59 -0800 Subject: [PATCH 282/384] Note preserveState behavior with multiple streams. (#315) --- packages/flutter_hooks/lib/src/async.dart | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/flutter_hooks/lib/src/async.dart b/packages/flutter_hooks/lib/src/async.dart index a199bc4d..92a93dbb 100644 --- a/packages/flutter_hooks/lib/src/async.dart +++ b/packages/flutter_hooks/lib/src/async.dart @@ -122,6 +122,11 @@ class _FutureStateHook extends HookState, _FutureHook> { /// * [preserveState] determines if the current value should be preserved when changing /// the [Stream] instance. /// +/// When [preserveState] is true (the default) update jank is reduced when switching +/// streams, but this may result in inconsistent state when using multiple +/// or nested streams. See https://github.com/rrousselGit/flutter_hooks/issues/312 +/// for more context. +/// /// See also: /// * [Stream], the object listened. /// * [useFuture], similar to [useStream] but for [Future]. From 13e8f4602e0635a22d481dbe329156a6966b18fa Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Sun, 19 Feb 2023 16:27:12 +0100 Subject: [PATCH 283/384] 0.18.6 --- packages/flutter_hooks/CHANGELOG.md | 5 +++-- packages/flutter_hooks/pubspec.yaml | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/flutter_hooks/CHANGELOG.md b/packages/flutter_hooks/CHANGELOG.md index bccbfc0a..4cd7f74c 100644 --- a/packages/flutter_hooks/CHANGELOG.md +++ b/packages/flutter_hooks/CHANGELOG.md @@ -1,7 +1,8 @@ -## Unreleased minor +## 0.18.6 - Added korean translation (thanks to @sejun2) - Add `useFocusScopeNode` hook (thanks to @iamsahilsonawane) + ## 0.18.5+1 Update links to the repository @@ -9,7 +10,7 @@ Update links to the repository ## 0.18.5 - Added `useListenableSelector`, similar to `useListenable`, for listening to a -`Listenable` but rebuilding the widget only if a certain data has changed (thanks to @ronnieeeeee) + `Listenable` but rebuilding the widget only if a certain data has changed (thanks to @ronnieeeeee) - Added `useAutomaticKeepAlive` (thanks to @DevNico) - Added `usePlatformBrightness` and `useOnPlatformBrightnessChange` to interact with platform `Brightness` (thanks to @AhmedLSayed9) diff --git a/packages/flutter_hooks/pubspec.yaml b/packages/flutter_hooks/pubspec.yaml index 765bfc41..e05c1e9b 100644 --- a/packages/flutter_hooks/pubspec.yaml +++ b/packages/flutter_hooks/pubspec.yaml @@ -3,7 +3,7 @@ description: A flutter implementation of React hooks. It adds a new kind of widg homepage: https://github.com/rrousselGit/flutter_hooks repository: https://github.com/rrousselGit/flutter_hooks/tree/master/packages/flutter_hooks issue_tracker: https://github.com/rrousselGit/flutter_hooks/issues -version: 0.18.5+1 +version: 0.18.6 environment: sdk: ">=2.17.0 <3.0.0" From 691368dc7329583be92edd89b2c2a939f184b83c Mon Sep 17 00:00:00 2001 From: Frankdroid7 Date: Wed, 8 Mar 2023 20:23:39 +0000 Subject: [PATCH 284/384] Fix typo (#346) --- packages/flutter_hooks/lib/src/primitives.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/flutter_hooks/lib/src/primitives.dart b/packages/flutter_hooks/lib/src/primitives.dart index 9838526f..97edb3f4 100644 --- a/packages/flutter_hooks/lib/src/primitives.dart +++ b/packages/flutter_hooks/lib/src/primitives.dart @@ -164,7 +164,7 @@ typedef Dispose = void Function(); /// /// [useEffect] is called synchronously on every `build`, unless /// [keys] is specified. In which case [useEffect] is called again only if -/// any value inside [keys] as changed. +/// any value inside [keys] has changed. /// /// It takes an [effect] callback and calls it synchronously. /// That [effect] may optionally return a function, which will be called when the [effect] is called again or if the widget is disposed. From f421a752e054a04c2f8828a04880f5df95b7a265 Mon Sep 17 00:00:00 2001 From: sunmkim <34917143+BGM-109@users.noreply.github.com> Date: Wed, 15 Mar 2023 18:34:12 +0900 Subject: [PATCH 285/384] ko_KR translate (#348) --- .../resources/translations/ko_kr/README.md | 236 +++++++++--------- 1 file changed, 116 insertions(+), 120 deletions(-) diff --git a/packages/flutter_hooks/resources/translations/ko_kr/README.md b/packages/flutter_hooks/resources/translations/ko_kr/README.md index 8bc851ee..b8655750 100644 --- a/packages/flutter_hooks/resources/translations/ko_kr/README.md +++ b/packages/flutter_hooks/resources/translations/ko_kr/README.md @@ -5,16 +5,15 @@ -# Flutter Hooks +# 플러터 훅 -A Flutter implementation of React hooks: https://medium.com/@dan_abramov/making-sense-of-react-hooks-fdbde8803889 +리액트 훅을 플러터에서 구현했을때 생기는 일: https://medium.com/@dan_abramov/making-sense-of-react-hooks-fdbde8803889 -훅은 `widget`의 라이프사이클을 관리하는 새로운 종류의 객체입니다. -훅은 한가지 이유로 존재합니다: 중복을 제거함으로써 위젯사이에 코드 공유를 증가시킵니다. +훅은 `widget` 의 생명주기를 관리하는 새로운 종류의 객체입니다. 훅이 존재하는 이유: 중복을 제거함으로써 위젯간 코드 생산성을 증가시킵니다. -## Motivation +## 제작 동기 -`StatefulWidget`는 아래와 같은 문제점이 있습니다: `initState`나 `dispose`와 같은 로직을 재사용하기가 매우 어렵습니다. 한가지 분명한 예시는 `AnimationController`입니다: +`StatefulWidget`은 아래와 같은 문제점이 있습니다: `initState` 나 `dispose`에서 사용된 로직을 재사용하기가 매우 어렵습니다. 적절한 예시는 `AnimationController`입니다: ```dart class Example extends StatefulWidget { @@ -57,12 +56,13 @@ class _ExampleState extends State with SingleTickerProviderStateMixin { } ``` -`AnimationController` 를 사용하는 모든 위젯은 이 로직을 재사용하기 위해 모두 이 로직을 재구현해야 합니다. 이는 물론 원치 않는 결과입니다. +`AnimationController` 를 사용하고 싶다면 사용하려는 모든 위젯에서 이 로직을 반복해야 합니다. 하지만 대부분 이를 원치 않을겁니다. -Dart mixins 는 이 문제를 해결할 수 있지만, 다른 문제점들이 있습니다: -- 주어진 mixin 은 한 클래스당 한번만 사용할 수 있습니다. -- mixin 과 클래스는 같은 객체를 공유합니다.\ - 이는 mixin 이 같은 이름의 변수를 정의하면, 컴파일 에러에서부터 알 수 없는 결과까지 다양한 결과를 가져올 수 있음을 의미합니다. +Dart Mixins 으로 이 문제를 해결할 수 있지만, 다른 문제점들이 있습니다: + +- Mixin은 한 클래스 당 한번만 사용할 수 있습니다. +- Mixin 과 클래스는 같은 객체를 공유합니다. + 예를 들어 두개의 Mixin 이 같은 이름일 때, 컴파일 에러에서부터 알 수 없는 결과까지 다양한 결과를 가져올 수 있음을 의미합니다. --- @@ -82,19 +82,18 @@ class Example extends HookWidget { } } ``` -이 코드는 이전 예제와 기능적으로 동일합니다. 여전히 `AnimationController` 를 dispose 하고, `Example.duration` 이 변경될 때 `duration` 을 업데이트합니다. -하지만 당신은 아마도 다음과 같은 생각을 하고 있을 것입니다: -> 모든 로직은 어디로 갔지? +이 코드는 위 예제와 기능적으로 동일합니다. 여전히 `AnimationController` 를 dispose 하고, `Example.duration` 이 변경될 때 `duration` 을 업데이트합니다. +당신은 아마도 다음과 같은 생각을 하고 있을 것입니다: + +> 다른 로직들은 어디에 있지? -이 로직은 `useAnimationController` 함수에 있습니다. 이 함수는 이 라이브러리에 포함되어 있습니다. -(see [Existing hooks](https://github.com/rrousselGit/flutter_hooks#existing-hooks)) - 이것이 우리가 훅이라고 부르는 것 입니다. +그 로직들은 `useAnimationController` 함수로 옮겨 졌습니다. 이 함수는 이 라이브러리에 내장되어 있습니다 ( [기본적인 훅들](https://github.com/rrousselGit/flutter_hooks#existing-hooks) 보기) - 이것이 훅 입니다. -훅은 몇가지 사양(Sepcification)을 가지고 있는 새로운 종류의 객체입니다. +훅은 몇가지의 특별함(Sepcification)을 가지고 있는 새로운 종류의 객체입니다. -- 훅은 `Hooks` 를 mix-in 한 위젯의 `build` 메소드에서만 사용할 수 있습니다. -- 동일한 훅은 임의의 수만큼 재사용될 수 있습니다. - 아래 코드는 두개의 독립적인 `AnimationController` 를 정의합니다. 그리고 위젯이 리빌드 될 때 이것들이 올바르게 보존됩니다. +- Mixin한 위젯의 `build` 메소드 안에서만 사용할 수 있습니다. +- 동일한 훅이라도 여러번 재사용될 수 있습니다. 아래에는 두개의 `AnimationController` 가 있습니다. 각각의 훅은 위젯이 리빌드 될 때 다른 훅의 상태를 보존합니다. ```dart Widget build(BuildContext context) { @@ -103,18 +102,18 @@ class Example extends HookWidget { return Container(); } ``` -- 훅은 서로와 위젯에 완전히 독립적입니다. + +- 훅과 훅, 훅과 위젯은 완전하게 독립적입니다. 이것은 훅을 패키지로 추출하고 [pub](https://pub.dartlang.org/) 에서 다른 사람들이 사용할 수 있도록 쉽게 만들어 줍니다. -## Principle +## 원리 -`State`와 비슷하게 훅은 `Widget`의 `Element`에 저장됩니다. 그러나 `State` 하나만 갖는 것 대신에, `Element`는 `List`를 갖습니다. 그리고 훅을 사용하기 위해서는 `Hook.use`를 호출해야 합니다. +`State`와 유사한 점은, 훅은 `Element` 라는 `Widget` 에 저장됩니다. 다른 점은 `State` 하나만 갖는 것 대신에, `Element`는 `List`에 저장합니다. 그리고 훅을 사용하기 위해서는 `Hook.use`이라고 호출합니다. `use` 함수에 의해 반환된 훅은 `use`가 호출된 횟수에 기반합니다. 첫번째 호출은 첫번째 훅을 반환하고, 두번째 호출은 두번째 훅을 반환하고, 세번째 호출은 세번째 훅을 반환하며 이런식으로 진행됩니다. -만약 이 아이디어가 아직도 이해가 안된다면, 아래와 같이 훅을 구현하는 것이 가능합니다: - +만약 이 개념이 이해가 안된다면, 아래를 보고 훅이 어떻게 구현되었는지 확인해보세요: ```dart class HookElement extends Element { @@ -133,23 +132,23 @@ class HookElement extends Element { 훅을 구현하는 더 다양한 예시를 보기위해 React 에서 훅이 어떻게 구현되어 있는지 훌륭한 글이 있습니다: https://medium.com/@ryardley/react-hooks-not-magic-just-arrays-cd4f1857236e -## Rules +## 규칙 -훅이 인덱스로부터 얻어지기 때문에, 몇가지 규칙을 지켜야 합니다: +훅은 리스트안에 존재하고, 인덱스로 불러오기 때문에, 몇가지 규칙을 지켜야합니다.: -### `use` 로 시작하는 이름을 사용하세요: +### 이름을 지을 때는 `use` 로 시작하시오: ```dart Widget build(BuildContext context) { - // starts with `use`, good name + // `use`로 시작했다면, 굿입니다. useMyHook(); - // doesn't start with `use`, could confuse people into thinking that this isn't a hook + // `use`로 시작하지 않기 때문에, 훅을 사용하는 사람들 사이에서 헷갈릴 수 있습니다. myHook(); // .... } ``` -### 훅을 조건 없이 호출하세요 +### 조건문 없이 호출하시오: ```dart Widget build(BuildContext context) { @@ -158,7 +157,7 @@ Widget build(BuildContext context) { } ``` -### `use`를 조건문 안에 넣지 마세요 +### `use`를 조건문 안에 넣지 마시오: ```dart Widget build(BuildContext context) { @@ -171,14 +170,13 @@ Widget build(BuildContext context) { --- -### About hot-reload +### 핫리로드에 대해서 -훅은 인덱스로부터 얻어지기 때문에, 리팩토링을 하면서 핫 리로드가 어플리케이션을 깨뜨릴 수 있을 것 같다고 생각할 수 있습니다. +훅은 인덱스로부터 얻어지기 때문에, 코드를 수정 하고 핫 리로드를 실행하면 앱이 멈춘다고 생각할 수도 있습니다. -하지만 걱정하지 마세요, `HookWidget` 은 기본 핫 리로드 동작을 훅과 함께 작동하도록 재정의합니다. 그래도, 훅의 상태가 리셋될 수 있는 상황이 있습니다. - -아래의 훅 리스트를 생각해보세요: +걱정하지 마세요, `HookWidget` 은 핫 리로드 시에도 훅의 상태들이 유지될 수 있도록 재정의 합니다.. 그럼에도, 훅의 상태가 리셋될 수 있는 상황이 있습니다. +아래의 훅 리스트를 보세요: ```dart useA(); @@ -186,8 +184,7 @@ useB(0); useC(); ``` -그런다음 hot-reload를 수행한 뒤 `HookB` 의 파라미터를 편집했다고 가정해봅시다: - +그 다음, 핫 리로드가 실행 된 후에 `HookB` 의 값을 수정했다고 가정해봅시다: ```dart useA(); @@ -197,8 +194,7 @@ useC(); 모든 훅이 잘 작동하고, 모든 훅의 상태가 유지됩니다. -이제 `HookB`가 제거되었다고 생각해 봅시다. 우리는 이제 다음과 같은것을 가지게 됩니다: - +이제 `HookB`가 제거해 봅시다. 그러면: ```dart useA(); @@ -206,15 +202,15 @@ useC(); ``` 이 상황에서 `HookA` 는 상태를 유지하지만 `HookC` 는 리셋됩니다. -이것은 리팩토링 후 핫 리로드를 수행하면, 첫번째로 영향을 받은 행 이후의 모든 훅이 제거되기 때문에 발생합니다. -그래서 `HookC`가 `HookB` 뒤에 있기 때문에 제거됩니다. +이유는 코드를 수정 한 후 핫 리로드가 실행되면, 첫번째로 영향을 받은 행 이후의 모든 훅이 제거되기 때문입니다. +그래서 `HookC` 는 `HookB` 뒤에 있기 때문에 상태가 리셋됩니다. -## How to create a hook +## 훅을 생성하는 법 훅을 생성하기위한 두가지 방법이 있습니다: - 함수 - 함수는 훅을 작성하는 가장 일반적인 방법입니다. 훅이 자연스럽게 합성 가능하기 때문에, 함수는 다른 훅을 결합하여 더 복잡한 커스텀 훅을 만들 수 있습니다. 관례상, 이러한 함수는 `use`로 시작됩니다. + 함수는 훅을 작성하는 가장 일반적인 방법입니다. 훅이 자연스럽게 합성 가능한 덕분에, 함수는 다른 훅을 결합하여 더 복잡한 커스텀 훅을 만들 수 있습니다. 관례상, 이러한 함수는 `use`로 시작됩니다. 아래의 코드는 변수를 생성하고, 값이 변경될 때마다 콘솔에 로그를 남기는 커스텀 훅을 정의합니다: @@ -230,8 +226,8 @@ useC(); - 클래스 - 훅이 너무 복잡해지면, `Hook` 을 확장하는 클래스로 변환할 수 있습니다. 이 클래스는 `Hook.use`를 사용하여 사용할 수 있습니다. - 클래스로 훅을 정의하면, 훅은 `State` 클래스와 매우 유사하게 보일 것이며 `initHook`, `dispose` 및 `setState`와 같은 위젯의 라이프 사이클 및 메서드에 액세스 할 수 있습니다. + 훅이 너무 복잡해지면, `Hook` 을 확장하는 클래스로 변환할 수 있습니다. 이 클래스는 `Hook.use` 함수로 사용할 수 있습니다. + 클래스로 훅을 정의하면, 훅은 `State` 클래스와 매우 유사하게 보일 것이며 `initHook`, `dispose` 및 `setState`와 같은 위젯의 라이프 사이클 및 메서드에 액세스 할 수 있습니다. 이와같이 함수 내에 클래스를 숨기는것은 좋은 예시입니다: @@ -241,7 +237,7 @@ useC(); } ``` - 아래의 코드는 `State`가 얼마나 오래 살아있었는지 콘솔에 출력하는 훅을 정의합니다: + 아래의 코드는 `State`가 생성되있었던 시간을 콘솔에 출력하는 훅을 정의합니다: ```dart class _TimeAlive extends Hook { @@ -271,98 +267,98 @@ useC(); } ``` -## Existing hooks +## 기본적인 훅들 Flutter_Hooks 는 이미 재사용 가능한 훅 목록을 제공합니다. 이 목록은 다음과 같이 구분됩니다: -### Primitives +### 원시적 -다른 위젯의 라이프사이클과 상호작용하는 low-level 의 훅 입니다. +다른 위젯의 생명주기에 반응하는 기초적인 훅 입니다. -| Name | Description | -| ----------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------- | -| [useEffect](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useEffect.html) | Useful for side-effects and optionally canceling them. | -| [useState](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useState.html) | Creates a variable and subscribes to it. | -| [useMemoized](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useMemoized.html) | Caches the instance of a complex object. | -| [useRef](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useRef.html) | Creates an object that contains a single mutable property. | -| [useCallback](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useCallback.html) | Caches a function instance. | -| [useContext](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useContext.html) | Obtains the `BuildContext` of the building `HookWidget`. | -| [useValueChanged](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useValueChanged.html) | Watches a value and triggers a callback whenever its value changed. | +| Name | Description | +| ----------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------ | +| [useEffect](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useEffect.html) | 상태를 업데이트하거나 선택적으로 취소하기에 유용합니다. | +| [useState](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useState.html) | 변수를 생성하고 구독합니다. | +| [useMemoized](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useMemoized.html) | 다양한 객체의 인스턴스를 캐싱합니다. | +| [useRef](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useRef.html) | 하나의 프로퍼티를 포함하는 객체를 만듭니다. | +| [useCallback](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useCallback.html) | 함수의 인스턴스를 캐싱합니다. | +| [useContext](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useContext.html) | `HookWidget` 의 `BuildContext`를 가져옵니다. | +| [useValueChanged](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useValueChanged.html) | 값을 모니터링하고, 값이 변경될 때마다 콜백함수를 실행합니다. | -### Object-binding +### 객체 바인딩 -이 카테고리의 훅은 기존의 Flutter/Dart 객체를 조작합니다. +해당 훅들은 Flutter/Dart에 이미 존재하는 객체들을 조작합니다. 이 훅은 객체를 생성/업데이트/삭제하는 역할을 합니다. -#### dart:async related hooks: - -| Name | Description | -| ------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------- | -| [useStream](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useStream.html) | Subscribes to a `Stream` and returns its current state as an `AsyncSnapshot`. | -| [useStreamController](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useStreamController.html) | Creates a `StreamController` which will automatically be disposed. | -| [useFuture](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useFuture.html) | Subscribes to a `Future` and returns its current state as an `AsyncSnapshot`. | - -#### Animation related hooks: - -| Name | Description | -| --------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------- | -| [useSingleTickerProvider](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useSingleTickerProvider.html) | Creates a single usage `TickerProvider`. | -| [useAnimationController](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useAnimationController.html) | Creates an `AnimationController` which will be automatically disposed. | -| [useAnimation](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useAnimation.html) | Subscribes to an `Animation` and returns its value. | - -#### Listenable related hooks: - -| Name | Description | -| ----------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------- | -| [useListenable](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useListenable.html) | Subscribes to a `Listenable` and marks the widget as needing build whenever the listener is called. | -| [useListenableSelector](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useListenableSelector.html) | Similar to `useListenable`, but allows filtering UI rebuilds | -| [useValueNotifier](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useValueNotifier.html) | Creates a `ValueNotifier` which will be automatically disposed. | -| [useValueListenable](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useValueListenable.html) | Subscribes to a `ValueListenable` and return its value. | - -#### Misc hooks: - -특정한 theme을 가지지 않는 일련의 훅 입니다. - -| Name | Description | -| ------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------- | -| [useReducer](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useReducer.html) | An alternative to `useState` for more complex states. | -| [usePrevious](https://pub.dartlang.org/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.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useFocusNode.html) | Creates a `FocusNode`. | -| [useTabController](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useTabController.html) | Creates and disposes a `TabController`. | -| [useScrollController](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useScrollController.html) | Creates and disposes a `ScrollController`. | -| [usePageController](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/usePageController.html) | Creates and disposes a `PageController`. | -| [useAppLifecycleState](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useAppLifecycleState.html) | Returns the current `AppLifecycleState` and rebuilds the widget on change. | -| [useOnAppLifecycleStateChange](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useOnAppLifecycleStateChange.html) | Listens to `AppLifecycleState` changes and triggers a callback on change. | -| [useTransformationController](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useTransformationController.html) | Creates and disposes a `TransformationController`. | -| [useIsMounted](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useIsMounted.html) | An equivalent to `State.mounted` for hooks. | -| [useAutomaticKeepAlive](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useAutomaticKeepAlive.html) | An equivalent to the `AutomaticKeepAlive` widget for hooks. | -| [useOnPlatformBrightnessChange](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useOnPlatformBrightnessChange.html) | Listens to platform `Brightness` changes and triggers a callback on change.| +#### dart:async 와 관련된 훅: + +| Name | Description | +| ------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------ | +| [useStream](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useStream.html) | `Stream`을 구독합니다. `AsyncSnapshot`으로 현재 상태를 반환합니다. | +| [useStreamController](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useStreamController.html) | 알아서 dispose되는 `StreamController` 를 생성합니다. | +| [useFuture](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useFuture.html) | `Future`를 구독합니다. `AsyncSnapshot`으로 상태를 반환합니다. | + +#### Animation 에 관련된 훅: + +| Name | Description | +| --------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------- | +| [useSingleTickerProvider](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useSingleTickerProvider.html) | `TickerProvider`를 생성합니다. | +| [useAnimationController](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useAnimationController.html) | 자동으로 dispose 되는 `AnimationController`를 생성합니다. | +| [useAnimation](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useAnimation.html) | `Animation` 를 구독합니다. 해당 객체의 value를 반환합니다. | + +#### Listenable 에 관련된 훅: + +| Name | Description | +| ----------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------ | +| [useListenable](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useListenable.html) | `Listenable` 을 구독합니다. 리스너가 호출될 때마다 위젯을 빌드가 필요한 것으로 표시합니다. | +| [useListenableSelector](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useListenableSelector.html) | Similar to `useListenable` 과 비슷하지만, 원하는 위젯만 변경되도록 선택할 수 있습니다.. | +| [useValueNotifier](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useValueNotifier.html) | 자동적으로 dispose 되는 `ValueNotifier`를 생성합니다. | +| [useValueListenable](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useValueListenable.html) | `ValueListenable` 를 구독합니다. 그 값을 반환합니다.. | + +#### 기타 훅: + +특별한 특징이 없는 훅들입니다. + +| Name | Description | +| --------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------- | +| [useReducer](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useReducer.html) | state가 조금 더 복잡할 때, `useState` 대신 사용할 대안 입니다. | +| [usePrevious](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/usePrevious.html) | 바로 이전에 실행된 [usePrevious]의 값을 반환합니다. | +| [useTextEditingController](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useTextEditingController-constant.html) | `TextEditingController`를 생성합니다. | +| [useFocusNode](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useFocusNode.html) | `FocusNode`를 생성합니다. | +| [useTabController](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useTabController.html) | `TabController`를 생성합니다. | +| [useScrollController](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useScrollController.html) | `ScrollController`를 생성합니다. | +| [usePageController](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/usePageController.html) | `PageController`를 생성합니다. | +| [useAppLifecycleState](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useAppLifecycleState.html) | 현재 `AppLifecycleState`를 반환합니다. 그리고 변화된 위젯을 다시 빌드합니다. | +| [useOnAppLifecycleStateChange](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useOnAppLifecycleStateChange.html) | Listens to `AppLifecycleState`가 변경될 때, 콜백함수를 실행합니다. | +| [useTransformationController](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useTransformationController.html) | Creates and disposes a `TransformationController`를 생성합니다. | +| [useIsMounted](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useIsMounted.html) | `State.mounted` 와 동일한 기능의 훅입니다. | +| [useAutomaticKeepAlive](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useAutomaticKeepAlive.html) | `AutomaticKeepAlive`와 동일한 훅입니다. | +| [useOnPlatformBrightnessChange](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useOnPlatformBrightnessChange.html) | 플랫폼의 `Brightness` 이 변경될 때, 콜백함수를 실행합니다. | ## Contributions -Contributions are welcomed! +기부를 환영합니다! -If you feel that a hook is missing, feel free to open a pull-request. +후크가 없는 것 같으면 풀 요청을 여십시오. -For a custom-hook to be merged, you will need to do the following: +사용자 지정 후크를 병합하려면 다음을 수행해야 합니다: -- Describe the use-case. +- 사용 사례를 설명합니다. - Open an issue explaining why we need this hook, how to use it, ... - This is important as a hook will not get merged if the hook doesn't appeal to - a large number of people. + 이 후크가 왜 필요한지, 어떻게 사용하는지 설명하는 문제를 엽니다... + 훅이 매력적이지 않으면 후크가 병합되지 않기 때문에 이것은 중요합니다 + 많은 사람들. - If your hook is rejected, don't worry! A rejection doesn't mean that it won't - be merged later in the future if more people show interest in it. - In the mean-time, feel free to publish your hook as a package on https://pub.dev. + 만약 여러분의 훅이 거절당하더라도, 걱정하지 마세요! 거절한다고 해서 거절당하지는 않을 것이다 + 더 많은 사람들이 그것에 관심을 보이면 나중에 합병된다. + 그동안 https://pub.dev에 당신의 후크를 패키지로 게시하세요. -- Write tests for your hook +- 후크에 대한 테스트 쓰기 - A hook will not be merged unless fully tested to avoid inadvertently breaking it - in the future. + 후크가 실수로 파손되지 않도록 완전히 테스트하지 않는 한 후크는 병합되지 않습니다 + 미래에. -- Add it to the README and write documentation for it. +- README에 추가하고 해당 문서를 작성합니다. ## Sponsors From bca197560b139861c113740ecf9efd55d75ef7d5 Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Wed, 10 May 2023 13:10:05 +0200 Subject: [PATCH 286/384] Update templates --- .github/ISSUE_TEMPLATE/bug_report.md | 2 ++ .github/ISSUE_TEMPLATE/config.yml | 4 ++-- .github/ISSUE_TEMPLATE/example_request.md | 2 ++ .github/ISSUE_TEMPLATE/feature_request.md | 2 ++ 4 files changed, 8 insertions(+), 2 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 666296cf..20065d84 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -3,6 +3,8 @@ name: Bug report about: There is a problem in how provider behaves title: "" labels: bug, needs triage +assignees: + - rrousselGit --- **Describe the bug** diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index c5059cc0..e5e64dc7 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1,5 +1,5 @@ blank_issues_enabled: false contact_links: - name: I have a problem and I need help - url: https://stackoverflow.com/questions/tagged/flutter - about: Please ask and answer questions here. \ No newline at end of file + url: https://github.com/rrousselGit/riverpod/discussions + about: Pleast ask and answer questions here diff --git a/.github/ISSUE_TEMPLATE/example_request.md b/.github/ISSUE_TEMPLATE/example_request.md index 20c7db06..332337ba 100644 --- a/.github/ISSUE_TEMPLATE/example_request.md +++ b/.github/ISSUE_TEMPLATE/example_request.md @@ -5,6 +5,8 @@ about: >- existing one. title: "" labels: documentation, needs triage +assignees: + - rrousselGit --- **Describe what scenario you think is uncovered by the existing examples/articles** diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index b6157e79..65c5ae35 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -3,6 +3,8 @@ name: Feature request about: Suggest an idea for this project title: "" labels: enhancement, needs triage +assignees: + - rrousselGit --- **Is your feature request related to a problem? Please describe.** From 38e45996d25d394484c15c5f9b2837ab2423a05d Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Thu, 11 May 2023 16:37:18 +0200 Subject: [PATCH 287/384] Make example null-safe (#357) --- .github/workflows/build.yml | 6 +- .../example/lib/custom_hook_function.dart | 2 +- packages/flutter_hooks/example/lib/main.dart | 5 +- .../example/lib/star_wars/models.dart | 6 +- .../example/lib/star_wars/models.g.dart | 92 ++++++++++--------- .../example/lib/star_wars/planet_screen.dart | 18 ++-- .../example/lib/star_wars/redux.dart | 10 +- .../example/lib/star_wars/redux.g.dart | 41 +++++---- .../example/lib/star_wars/star_wars_api.dart | 4 +- .../flutter_hooks/example/lib/use_effect.dart | 4 +- .../example/lib/use_reducer.dart | 9 +- packages/flutter_hooks/example/pubspec.yaml | 2 +- 12 files changed, 110 insertions(+), 89 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 3dd35503..3bbc433b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -17,6 +17,7 @@ jobs: - flutter_hooks channel: - stable + - master steps: - uses: actions/checkout@v2 @@ -29,11 +30,12 @@ jobs: working-directory: packages/${{ matrix.package }} - name: Check format - run: flutter format --set-exit-if-changed . + run: dart format --set-exit-if-changed . + if: matrix.channel == 'master' working-directory: packages/${{ matrix.package }} - name: Analyze - run: flutter analyze + run: dart analyze . working-directory: packages/${{ matrix.package }} - name: Run tests diff --git a/packages/flutter_hooks/example/lib/custom_hook_function.dart b/packages/flutter_hooks/example/lib/custom_hook_function.dart index 168de53c..cd2453f1 100644 --- a/packages/flutter_hooks/example/lib/custom_hook_function.dart +++ b/packages/flutter_hooks/example/lib/custom_hook_function.dart @@ -33,7 +33,7 @@ class CustomHookFunctionExample extends HookWidget { /// A custom hook that wraps the useState hook to add logging. Hooks can be /// composed -- meaning you can use hooks within hooks! -ValueNotifier useLoggedState([T initialData]) { +ValueNotifier useLoggedState(T initialData) { // First, call the useState hook. It will create a ValueNotifier for you that // rebuilds the Widget whenever the value changes. final result = useState(initialData); diff --git a/packages/flutter_hooks/example/lib/main.dart b/packages/flutter_hooks/example/lib/main.dart index 58c40afa..44c14cdf 100644 --- a/packages/flutter_hooks/example/lib/main.dart +++ b/packages/flutter_hooks/example/lib/main.dart @@ -46,7 +46,10 @@ class HooksGalleryApp extends HookWidget { } class _GalleryItem extends StatelessWidget { - const _GalleryItem({this.title, this.builder}); + const _GalleryItem({ + required this.title, + required this.builder, + }); final String title; final WidgetBuilder builder; diff --git a/packages/flutter_hooks/example/lib/star_wars/models.dart b/packages/flutter_hooks/example/lib/star_wars/models.dart index fe2370ff..c7746231 100644 --- a/packages/flutter_hooks/example/lib/star_wars/models.dart +++ b/packages/flutter_hooks/example/lib/star_wars/models.dart @@ -28,11 +28,9 @@ abstract class PlanetPageModel static Serializer get serializer => _$planetPageModelSerializer; - @nullable - String get next; + String? get next; - @nullable - String get previous; + String? get previous; BuiltList get results; } diff --git a/packages/flutter_hooks/example/lib/star_wars/models.g.dart b/packages/flutter_hooks/example/lib/star_wars/models.g.dart index dd7ab833..f4e07ffa 100644 --- a/packages/flutter_hooks/example/lib/star_wars/models.g.dart +++ b/packages/flutter_hooks/example/lib/star_wars/models.g.dart @@ -25,15 +25,15 @@ class _$PlanetPageModelSerializer final String wireName = 'PlanetPageModel'; @override - Iterable serialize(Serializers serializers, PlanetPageModel object, + Iterable serialize(Serializers serializers, PlanetPageModel object, {FullType specifiedType = FullType.unspecified}) { - final result = [ + final result = [ 'results', serializers.serialize(object.results, specifiedType: const FullType(BuiltList, const [const FullType(PlanetModel)])), ]; - Object value; + Object? value; value = object.next; if (value != null) { result @@ -53,29 +53,29 @@ class _$PlanetPageModelSerializer @override PlanetPageModel deserialize( - Serializers serializers, Iterable serialized, + Serializers serializers, Iterable serialized, {FullType specifiedType = FullType.unspecified}) { final result = new PlanetPageModelBuilder(); final iterator = serialized.iterator; while (iterator.moveNext()) { - final key = iterator.current as String; + final key = iterator.current! as String; iterator.moveNext(); - final Object value = iterator.current; + final Object? value = iterator.current; switch (key) { case 'next': result.next = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String)) as String?; break; case 'previous': result.previous = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String)) as String?; break; case 'results': result.results.replace(serializers.deserialize(value, specifiedType: const FullType( - BuiltList, const [const FullType(PlanetModel)])) - as BuiltList); + BuiltList, const [const FullType(PlanetModel)]))! + as BuiltList); break; } } @@ -91,9 +91,9 @@ class _$PlanetModelSerializer implements StructuredSerializer { final String wireName = 'PlanetModel'; @override - Iterable serialize(Serializers serializers, PlanetModel object, + Iterable serialize(Serializers serializers, PlanetModel object, {FullType specifiedType = FullType.unspecified}) { - final result = [ + final result = [ 'name', serializers.serialize(object.name, specifiedType: const FullType(String)), ]; @@ -102,19 +102,19 @@ class _$PlanetModelSerializer implements StructuredSerializer { } @override - PlanetModel deserialize(Serializers serializers, Iterable serialized, + PlanetModel deserialize(Serializers serializers, Iterable serialized, {FullType specifiedType = FullType.unspecified}) { final result = new PlanetModelBuilder(); final iterator = serialized.iterator; while (iterator.moveNext()) { - final key = iterator.current as String; + final key = iterator.current! as String; iterator.moveNext(); - final Object value = iterator.current; + final Object? value = iterator.current; switch (key) { case 'name': result.name = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String))! as String; break; } } @@ -125,16 +125,17 @@ class _$PlanetModelSerializer implements StructuredSerializer { class _$PlanetPageModel extends PlanetPageModel { @override - final String next; + final String? next; @override - final String previous; + final String? previous; @override final BuiltList results; - factory _$PlanetPageModel([void Function(PlanetPageModelBuilder) updates]) => + factory _$PlanetPageModel([void Function(PlanetPageModelBuilder)? updates]) => (new PlanetPageModelBuilder()..update(updates))._build(); - _$PlanetPageModel._({this.next, this.previous, this.results}) : super._() { + _$PlanetPageModel._({this.next, this.previous, required this.results}) + : super._() { BuiltValueNullFieldError.checkNotNull( results, r'PlanetPageModel', 'results'); } @@ -158,8 +159,12 @@ class _$PlanetPageModel extends PlanetPageModel { @override int get hashCode { - return $jf( - $jc($jc($jc(0, next.hashCode), previous.hashCode), results.hashCode)); + var _$hash = 0; + _$hash = $jc(_$hash, next.hashCode); + _$hash = $jc(_$hash, previous.hashCode); + _$hash = $jc(_$hash, results.hashCode); + _$hash = $jf(_$hash); + return _$hash; } @override @@ -174,20 +179,20 @@ class _$PlanetPageModel extends PlanetPageModel { class PlanetPageModelBuilder implements Builder { - _$PlanetPageModel _$v; + _$PlanetPageModel? _$v; - String _next; - String get next => _$this._next; - set next(String next) => _$this._next = next; + String? _next; + String? get next => _$this._next; + set next(String? next) => _$this._next = next; - String _previous; - String get previous => _$this._previous; - set previous(String previous) => _$this._previous = previous; + String? _previous; + String? get previous => _$this._previous; + set previous(String? previous) => _$this._previous = previous; - ListBuilder _results; + ListBuilder? _results; ListBuilder get results => _$this._results ??= new ListBuilder(); - set results(ListBuilder results) => _$this._results = results; + set results(ListBuilder? results) => _$this._results = results; PlanetPageModelBuilder(); @@ -209,7 +214,7 @@ class PlanetPageModelBuilder } @override - void update(void Function(PlanetPageModelBuilder) updates) { + void update(void Function(PlanetPageModelBuilder)? updates) { if (updates != null) updates(this); } @@ -223,7 +228,7 @@ class PlanetPageModelBuilder new _$PlanetPageModel._( next: next, previous: previous, results: results.build()); } catch (_) { - String _$failedField; + late String _$failedField; try { _$failedField = 'results'; results.build(); @@ -242,10 +247,10 @@ class _$PlanetModel extends PlanetModel { @override final String name; - factory _$PlanetModel([void Function(PlanetModelBuilder) updates]) => + factory _$PlanetModel([void Function(PlanetModelBuilder)? updates]) => (new PlanetModelBuilder()..update(updates))._build(); - _$PlanetModel._({this.name}) : super._() { + _$PlanetModel._({required this.name}) : super._() { BuiltValueNullFieldError.checkNotNull(name, r'PlanetModel', 'name'); } @@ -264,7 +269,10 @@ class _$PlanetModel extends PlanetModel { @override int get hashCode { - return $jf($jc(0, name.hashCode)); + var _$hash = 0; + _$hash = $jc(_$hash, name.hashCode); + _$hash = $jf(_$hash); + return _$hash; } @override @@ -275,11 +283,11 @@ class _$PlanetModel extends PlanetModel { } class PlanetModelBuilder implements Builder { - _$PlanetModel _$v; + _$PlanetModel? _$v; - String _name; - String get name => _$this._name; - set name(String name) => _$this._name = name; + String? _name; + String? get name => _$this._name; + set name(String? name) => _$this._name = name; PlanetModelBuilder(); @@ -299,7 +307,7 @@ class PlanetModelBuilder implements Builder { } @override - void update(void Function(PlanetModelBuilder) updates) { + void update(void Function(PlanetModelBuilder)? updates) { if (updates != null) updates(this); } @@ -316,4 +324,4 @@ class PlanetModelBuilder implements Builder { } } -// ignore_for_file: always_put_control_body_on_new_line,always_specify_types,annotate_overrides,avoid_annotating_with_dynamic,avoid_as,avoid_catches_without_on_clauses,avoid_returning_this,deprecated_member_use_from_same_package,lines_longer_than_80_chars,no_leading_underscores_for_local_identifiers,omit_local_variable_types,prefer_expression_function_bodies,sort_constructors_first,test_types_in_equals,unnecessary_const,unnecessary_new,unnecessary_lambdas +// ignore_for_file: deprecated_member_use_from_same_package,type=lint diff --git a/packages/flutter_hooks/example/lib/star_wars/planet_screen.dart b/packages/flutter_hooks/example/lib/star_wars/planet_screen.dart index 245c40b9..90852d6e 100644 --- a/packages/flutter_hooks/example/lib/star_wars/planet_screen.dart +++ b/packages/flutter_hooks/example/lib/star_wars/planet_screen.dart @@ -10,12 +10,12 @@ import 'star_wars_api.dart'; class _PlanetHandler { _PlanetHandler(this._store, this._starWarsApi); - final Store _store; + final Store _store; final StarWarsApi _starWarsApi; /// This will load all planets and will dispatch all necessary actions /// on the redux store. - Future fetchAndDispatch([String url]) async { + Future fetchAndDispatch([String? url]) async { _store.dispatch(FetchPlanetPageActionStart()); try { final page = await _starWarsApi.getPlanets(url); @@ -36,7 +36,7 @@ class PlanetScreen extends HookWidget { Widget build(BuildContext context) { final api = useMemoized(() => StarWarsApi()); - final store = useReducer( + final store = useReducer( reducer, initialState: AppState(), initialAction: null, @@ -92,16 +92,19 @@ class _PlanetScreenBody extends HookWidget { } class _Error extends StatelessWidget { - const _Error({Key key, this.errorMsg}) : super(key: key); + const _Error({ + Key? key, + required this.errorMsg, + }) : super(key: key); - final String errorMsg; + final String? errorMsg; @override Widget build(BuildContext context) { return Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - if (errorMsg != null) Text(errorMsg), + if (errorMsg != null) Text(errorMsg!), ElevatedButton( style: ButtonStyle( backgroundColor: MaterialStateProperty.all(Colors.redAccent), @@ -120,8 +123,7 @@ class _Error extends StatelessWidget { } class _LoadPageButton extends HookWidget { - const _LoadPageButton({this.next = true}) - : assert(next != null, 'next cannot be null'); + const _LoadPageButton({this.next = true}); final bool next; diff --git a/packages/flutter_hooks/example/lib/star_wars/redux.dart b/packages/flutter_hooks/example/lib/star_wars/redux.dart index 110ef94a..e0edab3a 100644 --- a/packages/flutter_hooks/example/lib/star_wars/redux.dart +++ b/packages/flutter_hooks/example/lib/star_wars/redux.dart @@ -28,7 +28,7 @@ class FetchPlanetPageActionSuccess extends ReduxAction { @immutable abstract class AppState implements Built { - factory AppState([void Function(AppStateBuilder) updates]) => + factory AppState([void Function(AppStateBuilder)? updates]) => _$AppState((u) => u ..isFetchingPlanets = false ..update(updates)); @@ -37,13 +37,15 @@ abstract class AppState implements Built { bool get isFetchingPlanets; - @nullable - String get errorFetchingPlanets; + String? get errorFetchingPlanets; PlanetPageModel get planetPage; } -AppState reducer(S state, A action) { +AppState reducer( + S state, + A action, +) { final b = state.toBuilder(); if (action is FetchPlanetPageActionStart) { b diff --git a/packages/flutter_hooks/example/lib/star_wars/redux.g.dart b/packages/flutter_hooks/example/lib/star_wars/redux.g.dart index df832749..9e122a93 100644 --- a/packages/flutter_hooks/example/lib/star_wars/redux.g.dart +++ b/packages/flutter_hooks/example/lib/star_wars/redux.g.dart @@ -10,15 +10,17 @@ class _$AppState extends AppState { @override final bool isFetchingPlanets; @override - final String errorFetchingPlanets; + final String? errorFetchingPlanets; @override final PlanetPageModel planetPage; - factory _$AppState([void Function(AppStateBuilder) updates]) => + factory _$AppState([void Function(AppStateBuilder)? updates]) => (new AppStateBuilder()..update(updates))._build(); _$AppState._( - {this.isFetchingPlanets, this.errorFetchingPlanets, this.planetPage}) + {required this.isFetchingPlanets, + this.errorFetchingPlanets, + required this.planetPage}) : super._() { BuiltValueNullFieldError.checkNotNull( isFetchingPlanets, r'AppState', 'isFetchingPlanets'); @@ -44,9 +46,12 @@ class _$AppState extends AppState { @override int get hashCode { - return $jf($jc( - $jc($jc(0, isFetchingPlanets.hashCode), errorFetchingPlanets.hashCode), - planetPage.hashCode)); + var _$hash = 0; + _$hash = $jc(_$hash, isFetchingPlanets.hashCode); + _$hash = $jc(_$hash, errorFetchingPlanets.hashCode); + _$hash = $jc(_$hash, planetPage.hashCode); + _$hash = $jf(_$hash); + return _$hash; } @override @@ -60,22 +65,22 @@ class _$AppState extends AppState { } class AppStateBuilder implements Builder { - _$AppState _$v; + _$AppState? _$v; - bool _isFetchingPlanets; - bool get isFetchingPlanets => _$this._isFetchingPlanets; - set isFetchingPlanets(bool isFetchingPlanets) => + bool? _isFetchingPlanets; + bool? get isFetchingPlanets => _$this._isFetchingPlanets; + set isFetchingPlanets(bool? isFetchingPlanets) => _$this._isFetchingPlanets = isFetchingPlanets; - String _errorFetchingPlanets; - String get errorFetchingPlanets => _$this._errorFetchingPlanets; - set errorFetchingPlanets(String errorFetchingPlanets) => + String? _errorFetchingPlanets; + String? get errorFetchingPlanets => _$this._errorFetchingPlanets; + set errorFetchingPlanets(String? errorFetchingPlanets) => _$this._errorFetchingPlanets = errorFetchingPlanets; - PlanetPageModelBuilder _planetPage; + PlanetPageModelBuilder? _planetPage; PlanetPageModelBuilder get planetPage => _$this._planetPage ??= new PlanetPageModelBuilder(); - set planetPage(PlanetPageModelBuilder planetPage) => + set planetPage(PlanetPageModelBuilder? planetPage) => _$this._planetPage = planetPage; AppStateBuilder(); @@ -98,7 +103,7 @@ class AppStateBuilder implements Builder { } @override - void update(void Function(AppStateBuilder) updates) { + void update(void Function(AppStateBuilder)? updates) { if (updates != null) updates(this); } @@ -115,7 +120,7 @@ class AppStateBuilder implements Builder { errorFetchingPlanets: errorFetchingPlanets, planetPage: planetPage.build()); } catch (_) { - String _$failedField; + late String _$failedField; try { _$failedField = 'planetPage'; planetPage.build(); @@ -130,4 +135,4 @@ class AppStateBuilder implements Builder { } } -// ignore_for_file: always_put_control_body_on_new_line,always_specify_types,annotate_overrides,avoid_annotating_with_dynamic,avoid_as,avoid_catches_without_on_clauses,avoid_returning_this,deprecated_member_use_from_same_package,lines_longer_than_80_chars,no_leading_underscores_for_local_identifiers,omit_local_variable_types,prefer_expression_function_bodies,sort_constructors_first,test_types_in_equals,unnecessary_const,unnecessary_new,unnecessary_lambdas +// ignore_for_file: deprecated_member_use_from_same_package,type=lint diff --git a/packages/flutter_hooks/example/lib/star_wars/star_wars_api.dart b/packages/flutter_hooks/example/lib/star_wars/star_wars_api.dart index 9f0f7286..26079874 100644 --- a/packages/flutter_hooks/example/lib/star_wars/star_wars_api.dart +++ b/packages/flutter_hooks/example/lib/star_wars/star_wars_api.dart @@ -7,12 +7,12 @@ import 'models.dart'; /// Api wrapper to retrieve Star Wars related data class StarWarsApi { /// load and return one page of planets - Future getPlanets([String page]) async { + Future getPlanets([String? page]) async { page ??= 'https://swapi.dev/api/planets'; final response = await http.get(Uri.parse(page)); final dynamic json = jsonDecode(utf8.decode(response.bodyBytes)); - return serializers.deserializeWith(PlanetPageModel.serializer, json); + return serializers.deserializeWith(PlanetPageModel.serializer, json)!; } } diff --git a/packages/flutter_hooks/example/lib/use_effect.dart b/packages/flutter_hooks/example/lib/use_effect.dart index f0fcf1e9..2643bc5b 100644 --- a/packages/flutter_hooks/example/lib/use_effect.dart +++ b/packages/flutter_hooks/example/lib/use_effect.dart @@ -35,7 +35,7 @@ class CustomHookExample extends HookWidget { return !count.hasData ? const CircularProgressIndicator() : GestureDetector( - onTap: () => countController.add(count.data + 1), + onTap: () => countController.add(count.requireData + 1), child: Text('You tapped me ${count.data} times.'), ); }, @@ -79,7 +79,7 @@ StreamController _useLocalStorageInt( useEffect( () { SharedPreferences.getInstance().then((prefs) async { - final int valueFromStorage = prefs.getInt(key); + final int? valueFromStorage = prefs.getInt(key); controller.add(valueFromStorage ?? defaultValue); }).catchError(controller.addError); return null; diff --git a/packages/flutter_hooks/example/lib/use_reducer.dart b/packages/flutter_hooks/example/lib/use_reducer.dart index e365f0c4..e9e57d6d 100644 --- a/packages/flutter_hooks/example/lib/use_reducer.dart +++ b/packages/flutter_hooks/example/lib/use_reducer.dart @@ -14,15 +14,16 @@ class State { // Create the actions you wish to dispatch to the reducer class IncrementCounter { - IncrementCounter({this.counter}); - int counter; + IncrementCounter({required this.counter}); + + final int counter; } class UseReducerExample extends HookWidget { @override Widget build(BuildContext context) { // Create the reducer function that will handle the actions you dispatch - State _reducer(State state, IncrementCounter action) { + State _reducer(State state, IncrementCounter? action) { if (action is IncrementCounter) { return State(counter: state.counter + action.counter); } @@ -32,7 +33,7 @@ class UseReducerExample extends HookWidget { // Next, invoke the `useReducer` function with the reducer function and initial state to create a // `_store` variable that contains the current state and dispatch. Whenever the value is // changed, this Widget will be rebuilt! - final _store = useReducer( + final _store = useReducer( _reducer, initialState: const State(), initialAction: null, diff --git a/packages/flutter_hooks/example/pubspec.yaml b/packages/flutter_hooks/example/pubspec.yaml index 7c0a7c60..2ec419e9 100644 --- a/packages/flutter_hooks/example/pubspec.yaml +++ b/packages/flutter_hooks/example/pubspec.yaml @@ -5,7 +5,7 @@ publish_to: none version: 1.0.0+1 environment: - sdk: ">=2.7.0 <3.0.0-200.0.dev" + sdk: ">=2.12.0 <4.0.0" dependencies: built_collection: ^5.0.0 From a416e88afede12a48d1ba0e70271db2f988be977 Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Fri, 12 May 2023 10:03:58 +0200 Subject: [PATCH 288/384] Typo --- .github/ISSUE_TEMPLATE/config.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index e5e64dc7..e5779122 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1,5 +1,5 @@ blank_issues_enabled: false contact_links: - name: I have a problem and I need help - url: https://github.com/rrousselGit/riverpod/discussions - about: Pleast ask and answer questions here + url: https://github.com/rrousselGit/flutter_hooks/discussions + about: Please ask and answer questions here. From 185340528b77392f1590d4bdf13f219cc0fcb3f6 Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Wed, 24 May 2023 22:18:35 +0200 Subject: [PATCH 289/384] Add to project --- .github/workflows/project.yml | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 .github/workflows/project.yml diff --git a/.github/workflows/project.yml b/.github/workflows/project.yml new file mode 100644 index 00000000..902ec6dc --- /dev/null +++ b/.github/workflows/project.yml @@ -0,0 +1,21 @@ +name: Add new issues to project + +on: + issues: + types: + - opened + - reopened + pull_request: + types: + - opened + - reopened + +jobs: + add-to-project: + name: Add issue to project + runs-on: ubuntu-latest + steps: + - uses: actions/add-to-project@v0.5.0 + with: + project-url: https://github.com/users/rrousselGit/projects/8 + github-token: ${{ secrets.ADD_TO_PROJECT_PAT }} From e1479c8421b79a39f06d2bf8924d78e99192db0b Mon Sep 17 00:00:00 2001 From: snapsl <32878439+snapsl@users.noreply.github.com> Date: Mon, 3 Jul 2023 18:04:31 +0200 Subject: [PATCH 290/384] update pub.dartlang.org to pub.dev (#362) --- README.md | 84 +++++++++++++++++++++++++++---------------------------- 1 file changed, 42 insertions(+), 42 deletions(-) diff --git a/README.md b/README.md index d69d514f..c28a1583 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ [English](https://github.com/rrousselGit/flutter_hooks/blob/master/README.md) | [Português](https://github.com/rrousselGit/flutter_hooks/blob/master/packages/flutter_hooks/resources/translations/pt_br/README.md) | [한국어](https://github.com/rrousselGit/flutter_hooks/blob/master/packages/flutter_hooks/resources/translations/ko_kr/README.md) -[![Build](https://github.com/rrousselGit/flutter_hooks/workflows/Build/badge.svg)](https://github.com/rrousselGit/flutter_hooks/actions?query=workflow%3ABuild) [![codecov](https://codecov.io/gh/rrousselGit/flutter_hooks/branch/master/graph/badge.svg)](https://codecov.io/gh/rrousselGit/flutter_hooks) [![pub package](https://img.shields.io/pub/v/flutter_hooks.svg)](https://pub.dartlang.org/packages/flutter_hooks) [![pub package](https://img.shields.io/badge/Awesome-Flutter-blue.svg?longCache=true&style=flat-square)](https://github.com/Solido/awesome-flutter) +[![Build](https://github.com/rrousselGit/flutter_hooks/workflows/Build/badge.svg)](https://github.com/rrousselGit/flutter_hooks/actions?query=workflow%3ABuild) [![codecov](https://codecov.io/gh/rrousselGit/flutter_hooks/branch/master/graph/badge.svg)](https://codecov.io/gh/rrousselGit/flutter_hooks) [![pub package](https://img.shields.io/pub/v/flutter_hooks.svg)](https://pub.dev/packages/flutter_hooks) [![pub package](https://img.shields.io/badge/Awesome-Flutter-blue.svg?longCache=true&style=flat-square)](https://github.com/Solido/awesome-flutter) Discord @@ -113,7 +113,7 @@ Hooks are a new kind of object with some specificities: - Hooks are entirely independent of each other and from the widget.\ This means that they can easily be extracted into a package and published on - [pub](https://pub.dartlang.org/) for others to use. + [pub](https://pub.dev/) for others to use. ## Principle @@ -293,15 +293,15 @@ Flutter_Hooks already comes with a list of reusable hooks which are divided into A set of low-level hooks that interact with the different life-cycles of a widget -| Name | Description | -| ----------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------- | -| [useEffect](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useEffect.html) | Useful for side-effects and optionally canceling them. | -| [useState](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useState.html) | Creates a variable and subscribes to it. | -| [useMemoized](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useMemoized.html) | Caches the instance of a complex object. | -| [useRef](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useRef.html) | Creates an object that contains a single mutable property. | -| [useCallback](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useCallback.html) | Caches a function instance. | -| [useContext](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useContext.html) | Obtains the `BuildContext` of the building `HookWidget`. | -| [useValueChanged](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useValueChanged.html) | Watches a value and triggers a callback whenever its value changed. | +| Name | Description | +| -------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------- | +| [useEffect](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useEffect.html) | Useful for side-effects and optionally canceling them. | +| [useState](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useState.html) | Creates a variable and subscribes to it. | +| [useMemoized](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useMemoized.html) | Caches the instance of a complex object. | +| [useRef](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useRef.html) | Creates an object that contains a single mutable property. | +| [useCallback](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useCallback.html) | Caches a function instance. | +| [useContext](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useContext.html) | Obtains the `BuildContext` of the building `HookWidget`. | +| [useValueChanged](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useValueChanged.html) | Watches a value and triggers a callback whenever its value changed. | ### Object-binding @@ -310,48 +310,48 @@ They will take care of creating/updating/disposing an object. #### dart:async related hooks: -| Name | Description | -| ------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------- | -| [useStream](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useStream.html) | Subscribes to a `Stream` and returns its current state as an `AsyncSnapshot`. | -| [useStreamController](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useStreamController.html) | Creates a `StreamController` which will automatically be disposed. | -| [useFuture](https://pub.dartlang.org/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. | +| [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: -| Name | Description | -| --------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------- | -| [useSingleTickerProvider](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useSingleTickerProvider.html) | Creates a single usage `TickerProvider`. | -| [useAnimationController](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useAnimationController.html) | Creates an `AnimationController` which will be automatically disposed. | -| [useAnimation](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useAnimation.html) | Subscribes to an `Animation` and returns its value. | +| Name | Description | +| ------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------- | +| [useSingleTickerProvider](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useSingleTickerProvider.html) | Creates a single usage `TickerProvider`. | +| [useAnimationController](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useAnimationController.html) | Creates an `AnimationController` which will be automatically disposed. | +| [useAnimation](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useAnimation.html) | Subscribes to an `Animation` and returns its value. | #### Listenable related hooks: -| Name | Description | -| ----------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------- | -| [useListenable](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useListenable.html) | Subscribes to a `Listenable` and marks the widget as needing build whenever the listener is called. | -| [useListenableSelector](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useListenableSelector.html) | Similar to `useListenable`, but allows filtering UI rebuilds | -| [useValueNotifier](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useValueNotifier.html) | Creates a `ValueNotifier` which will be automatically disposed. | -| [useValueListenable](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useValueListenable.html) | Subscribes to a `ValueListenable` and return its value. | +| Name | Description | +| -------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------- | +| [useListenable](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useListenable.html) | Subscribes to a `Listenable` and marks the widget as needing build whenever the listener is called. | +| [useListenableSelector](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useListenableSelector.html) | Similar to `useListenable`, but allows filtering UI rebuilds | +| [useValueNotifier](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useValueNotifier.html) | Creates a `ValueNotifier` which will be automatically disposed. | +| [useValueListenable](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useValueListenable.html) | Subscribes to a `ValueListenable` and return its value. | #### Misc hooks: A series of hooks with no particular theme. -| Name | Description | -| ------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------- | -| [useReducer](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useReducer.html) | An alternative to `useState` for more complex states. | -| [usePrevious](https://pub.dartlang.org/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.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useFocusNode.html) | Creates a `FocusNode`. | -| [useTabController](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useTabController.html) | Creates and disposes a `TabController`. | -| [useScrollController](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useScrollController.html) | Creates and disposes a `ScrollController`. | -| [usePageController](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/usePageController.html) | Creates and disposes a `PageController`. | -| [useAppLifecycleState](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useAppLifecycleState.html) | Returns the current `AppLifecycleState` and rebuilds the widget on change. | -| [useOnAppLifecycleStateChange](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useOnAppLifecycleStateChange.html) | Listens to `AppLifecycleState` changes and triggers a callback on change. | -| [useTransformationController](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useTransformationController.html) | Creates and disposes a `TransformationController`. | -| [useIsMounted](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useIsMounted.html) | An equivalent to `State.mounted` for hooks. | -| [useAutomaticKeepAlive](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useAutomaticKeepAlive.html) | An equivalent to the `AutomaticKeepAlive` widget for hooks. | -| [useOnPlatformBrightnessChange](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useOnPlatformBrightnessChange.html) | Listens to platform `Brightness` changes and triggers a callback on change.| +| 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.| ## Contributions From eb3ff94bfc53a6b1acb3511a525ca4551fe13166 Mon Sep 17 00:00:00 2001 From: snapsl <32878439+snapsl@users.noreply.github.com> Date: Thu, 6 Jul 2023 14:15:45 +0200 Subject: [PATCH 291/384] Implement Hook for SearchController (#361) --- README.md | 1 + packages/flutter_hooks/CHANGELOG.md | 4 + packages/flutter_hooks/lib/src/hooks.dart | 20 ++-- .../lib/src/search_controller.dart | 31 +++++++ .../test/use_search_controller_test.dart | 92 +++++++++++++++++++ 5 files changed, 139 insertions(+), 9 deletions(-) create mode 100644 packages/flutter_hooks/lib/src/search_controller.dart create mode 100644 packages/flutter_hooks/test/use_search_controller_test.dart 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'); + }); + }); +} From ba32988f134ef0126be5852ce57ffabe53ba55b3 Mon Sep 17 00:00:00 2001 From: Jaesung Hong Date: Tue, 11 Jul 2023 03:09:37 +0900 Subject: [PATCH 292/384] Make useCallback hook's keys optional (#369) --- packages/flutter_hooks/CHANGELOG.md | 1 + .../flutter_hooks/lib/src/primitives.dart | 6 +- .../flutter_hooks/test/memoized_test.dart | 36 ---------- .../flutter_hooks/test/use_callback_test.dart | 72 +++++++++++++++++++ 4 files changed, 76 insertions(+), 39 deletions(-) create mode 100644 packages/flutter_hooks/test/use_callback_test.dart diff --git a/packages/flutter_hooks/CHANGELOG.md b/packages/flutter_hooks/CHANGELOG.md index 23f73e76..0c30365e 100644 --- a/packages/flutter_hooks/CHANGELOG.md +++ b/packages/flutter_hooks/CHANGELOG.md @@ -1,6 +1,7 @@ ## Unreleased minor - Added `useSearchController` (thanks to @snapsl) +- Keys on `useCallback` are now optional, to match `useMemoized` (thanks to @jezsung) ## 0.18.6 diff --git a/packages/flutter_hooks/lib/src/primitives.dart b/packages/flutter_hooks/lib/src/primitives.dart index 97edb3f4..31e742c0 100644 --- a/packages/flutter_hooks/lib/src/primitives.dart +++ b/packages/flutter_hooks/lib/src/primitives.dart @@ -42,9 +42,9 @@ ObjectRef useRef(T initialValue) { /// }, [key]); /// ``` T useCallback( - T callback, - List keys, -) { + T callback, [ + List keys = const [], +]) { return useMemoized(() => callback, keys); } diff --git a/packages/flutter_hooks/test/memoized_test.dart b/packages/flutter_hooks/test/memoized_test.dart index da5eda49..deeb9392 100644 --- a/packages/flutter_hooks/test/memoized_test.dart +++ b/packages/flutter_hooks/test/memoized_test.dart @@ -65,42 +65,6 @@ void main() { reason: 'The ref value has the last assigned value.'); }); - testWidgets('useCallback', (tester) async { - late int Function() fn; - - await tester.pumpWidget( - HookBuilder(builder: (context) { - fn = useCallback(() => 42, []); - return Container(); - }), - ); - - expect(fn(), 42); - - late int Function() fn2; - - await tester.pumpWidget( - HookBuilder(builder: (context) { - fn2 = useCallback(() => 42, []); - return Container(); - }), - ); - - expect(fn2, fn); - - late int Function() fn3; - - await tester.pumpWidget( - HookBuilder(builder: (context) { - fn3 = useCallback(() => 21, [42]); - return Container(); - }), - ); - - expect(fn3, isNot(fn)); - expect(fn3(), 21); - }); - testWidgets('memoized without parameter calls valueBuilder once', (tester) async { late int result; diff --git a/packages/flutter_hooks/test/use_callback_test.dart b/packages/flutter_hooks/test/use_callback_test.dart new file mode 100644 index 00000000..f5ec5520 --- /dev/null +++ b/packages/flutter_hooks/test/use_callback_test.dart @@ -0,0 +1,72 @@ +import 'package:flutter/widgets.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; + +import 'mock.dart'; + +void main() { + testWidgets('useCallback', (tester) async { + late int Function() fn; + + await tester.pumpWidget( + HookBuilder(builder: (context) { + fn = useCallback(() => 42, []); + return Container(); + }), + ); + + expect(fn(), 42); + + late int Function() fn2; + + await tester.pumpWidget( + HookBuilder(builder: (context) { + fn2 = useCallback(() => 42, []); + return Container(); + }), + ); + + expect(fn2, fn); + + late int Function() fn3; + + await tester.pumpWidget( + HookBuilder(builder: (context) { + fn3 = useCallback(() => 21, [42]); + return Container(); + }), + ); + + expect(fn3, isNot(fn)); + expect(fn3(), 21); + }); + + testWidgets( + 'should return same function when keys are not specified', + (tester) async { + late Function fn1; + late Function fn2; + + await tester.pumpWidget( + HookBuilder( + key: const Key('hook_builder'), + builder: (context) { + fn1 = useCallback(() {}); + return Container(); + }, + ), + ); + + await tester.pumpWidget( + HookBuilder( + key: const Key('hook_builder'), + builder: (context) { + fn2 = useCallback(() {}); + return Container(); + }, + ), + ); + + expect(fn1, same(fn2)); + }, + ); +} From 159b032d8cd070afaaa6d512f3f44b5c849bb23c Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Mon, 10 Jul 2023 20:17:25 +0200 Subject: [PATCH 293/384] Fix deprecated WidgetsBinding.window warning --- packages/flutter_hooks/CHANGELOG.md | 1 + packages/flutter_hooks/lib/src/platform_brightness.dart | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/flutter_hooks/CHANGELOG.md b/packages/flutter_hooks/CHANGELOG.md index 0c30365e..abfc20e4 100644 --- a/packages/flutter_hooks/CHANGELOG.md +++ b/packages/flutter_hooks/CHANGELOG.md @@ -2,6 +2,7 @@ - Added `useSearchController` (thanks to @snapsl) - Keys on `useCallback` are now optional, to match `useMemoized` (thanks to @jezsung) +- Update `usePlatformBrightness` to stop relying on deprecated code ## 0.18.6 diff --git a/packages/flutter_hooks/lib/src/platform_brightness.dart b/packages/flutter_hooks/lib/src/platform_brightness.dart index 66cf5636..291aff2f 100644 --- a/packages/flutter_hooks/lib/src/platform_brightness.dart +++ b/packages/flutter_hooks/lib/src/platform_brightness.dart @@ -42,7 +42,7 @@ class _PlatformBrightnessState @override void initHook() { super.initHook(); - _brightness = WidgetsBinding.instance.window.platformBrightness; + _brightness = WidgetsBinding.instance.platformDispatcher.platformBrightness; WidgetsBinding.instance.addObserver(this); } @@ -59,7 +59,7 @@ class _PlatformBrightnessState void didChangePlatformBrightness() { super.didChangePlatformBrightness(); final _previous = _brightness; - _brightness = WidgetsBinding.instance.window.platformBrightness; + _brightness = WidgetsBinding.instance.platformDispatcher.platformBrightness; hook.onBrightnessChange?.call(_previous, _brightness); if (hook.rebuildOnChange) { From 8ad7bcd1d50ad470f6ca1b93d626f0f1ca0d8b9d Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Mon, 10 Jul 2023 20:35:11 +0200 Subject: [PATCH 294/384] The following packages have been updated: flutter_hooks : 0.18.6 -> 0.19.0 --- packages/flutter_hooks/CHANGELOG.md | 2 +- packages/flutter_hooks/pubspec.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/flutter_hooks/CHANGELOG.md b/packages/flutter_hooks/CHANGELOG.md index abfc20e4..366057d5 100644 --- a/packages/flutter_hooks/CHANGELOG.md +++ b/packages/flutter_hooks/CHANGELOG.md @@ -1,4 +1,4 @@ -## Unreleased minor +## 0.19.0 - 2023-07-10 - Added `useSearchController` (thanks to @snapsl) - Keys on `useCallback` are now optional, to match `useMemoized` (thanks to @jezsung) diff --git a/packages/flutter_hooks/pubspec.yaml b/packages/flutter_hooks/pubspec.yaml index e05c1e9b..b7151161 100644 --- a/packages/flutter_hooks/pubspec.yaml +++ b/packages/flutter_hooks/pubspec.yaml @@ -3,7 +3,7 @@ description: A flutter implementation of React hooks. It adds a new kind of widg homepage: https://github.com/rrousselGit/flutter_hooks repository: https://github.com/rrousselGit/flutter_hooks/tree/master/packages/flutter_hooks issue_tracker: https://github.com/rrousselGit/flutter_hooks/issues -version: 0.18.6 +version: 0.19.0 environment: sdk: ">=2.17.0 <3.0.0" From bc65cfe8d16c1d34bd4a70dcbbbf06bcf1345c1f Mon Sep 17 00:00:00 2001 From: jeong hwan Cheon <111282517+HACCP92@users.noreply.github.com> Date: Wed, 12 Jul 2023 17:03:58 +0900 Subject: [PATCH 295/384] Fix typos (#371) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In Korean orthography, it means a period (.), which means the end of a sentence. (이 함수는 이 라이브러리에 내장되어 있습니다(X) => 있습니다.(O) ) The colon is missing when describing the code. (다른 훅의 상태를 보존합니다. (X) => 다른 훅의 상태를 보존합니다: (O) ) --- packages/flutter_hooks/resources/translations/ko_kr/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/flutter_hooks/resources/translations/ko_kr/README.md b/packages/flutter_hooks/resources/translations/ko_kr/README.md index b8655750..1f7c3952 100644 --- a/packages/flutter_hooks/resources/translations/ko_kr/README.md +++ b/packages/flutter_hooks/resources/translations/ko_kr/README.md @@ -88,12 +88,12 @@ class Example extends HookWidget { > 다른 로직들은 어디에 있지? -그 로직들은 `useAnimationController` 함수로 옮겨 졌습니다. 이 함수는 이 라이브러리에 내장되어 있습니다 ( [기본적인 훅들](https://github.com/rrousselGit/flutter_hooks#existing-hooks) 보기) - 이것이 훅 입니다. +그 로직들은 `useAnimationController` 함수로 옮겨 졌습니다. 이 함수는 이 라이브러리에 내장되어 있습니다. ( [기본적인 훅들](https://github.com/rrousselGit/flutter_hooks#existing-hooks) 보기) - 이것이 훅 입니다. 훅은 몇가지의 특별함(Sepcification)을 가지고 있는 새로운 종류의 객체입니다. - Mixin한 위젯의 `build` 메소드 안에서만 사용할 수 있습니다. -- 동일한 훅이라도 여러번 재사용될 수 있습니다. 아래에는 두개의 `AnimationController` 가 있습니다. 각각의 훅은 위젯이 리빌드 될 때 다른 훅의 상태를 보존합니다. +- 동일한 훅이라도 여러번 재사용될 수 있습니다. 아래에는 두개의 `AnimationController` 가 있습니다. 각각의 훅은 위젯이 리빌드 될 때 다른 훅의 상태를 보존합니다: ```dart Widget build(BuildContext context) { From ffee9e82f6bc22335e13a998981b470b6c037189 Mon Sep 17 00:00:00 2001 From: Jaesung Hong Date: Fri, 21 Jul 2023 17:59:53 +0900 Subject: [PATCH 296/384] Add useOnStreamChange hook (#373) --- README.md | 1 + packages/flutter_hooks/lib/src/async.dart | 95 ++++++ .../test/use_on_stream_change_test.dart | 293 ++++++++++++++++++ 3 files changed, 389 insertions(+) create mode 100644 packages/flutter_hooks/test/use_on_stream_change_test.dart diff --git a/README.md b/README.md index 6aaefa04..94fc208f 100644 --- a/README.md +++ b/README.md @@ -314,6 +314,7 @@ They will take care of creating/updating/disposing an object. | ---------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------- | | [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: diff --git a/packages/flutter_hooks/lib/src/async.dart b/packages/flutter_hooks/lib/src/async.dart index 92a93dbb..92ed39d8 100644 --- a/packages/flutter_hooks/lib/src/async.dart +++ b/packages/flutter_hooks/lib/src/async.dart @@ -320,3 +320,98 @@ class _StreamControllerHookState @override String get debugLabel => 'useStreamController'; } + +/// Subscribes to a [Stream] and calls the [Stream.listen] to register the [onData], +/// [onError], and [onDone]. +/// +/// Returns the [StreamSubscription] returned by the [Stream.listen]. +/// +/// See also: +/// * [Stream], the object listened. +/// * [Stream.listen], calls the provided handlers. +StreamSubscription? useOnStreamChange( + Stream? stream, { + void Function(T event)? onData, + void Function(Object error, StackTrace stackTrace)? onError, + void Function()? onDone, + bool? cancelOnError, +}) { + return use?>( + _OnStreamChangeHook( + stream, + onData: onData, + onError: onError, + onDone: onDone, + cancelOnError: cancelOnError, + ), + ); +} + +class _OnStreamChangeHook extends Hook?> { + const _OnStreamChangeHook( + this.stream, { + this.onData, + this.onError, + this.onDone, + this.cancelOnError, + }); + + final Stream? stream; + final void Function(T event)? onData; + final void Function(Object error, StackTrace stackTrace)? onError; + final void Function()? onDone; + final bool? cancelOnError; + + @override + _StreamListenerHookState createState() => _StreamListenerHookState(); +} + +class _StreamListenerHookState + extends HookState?, _OnStreamChangeHook> { + StreamSubscription? _subscription; + + @override + void initHook() { + super.initHook(); + _subscribe(); + } + + @override + void didUpdateHook(_OnStreamChangeHook oldWidget) { + super.didUpdateHook(oldWidget); + if (oldWidget.stream != hook.stream || + oldWidget.cancelOnError != hook.cancelOnError) { + _unsubscribe(); + _subscribe(); + } + } + + @override + void dispose() { + _unsubscribe(); + } + + void _subscribe() { + final stream = hook.stream; + if (stream != null) { + _subscription = stream.listen( + hook.onData, + onError: hook.onError, + onDone: hook.onDone, + cancelOnError: hook.cancelOnError, + ); + } + } + + void _unsubscribe() { + _subscription?.cancel(); + } + + @override + StreamSubscription? build(BuildContext context) { + return _subscription; + } + + @override + String get debugLabel => 'useOnStreamChange'; +} diff --git a/packages/flutter_hooks/test/use_on_stream_change_test.dart b/packages/flutter_hooks/test/use_on_stream_change_test.dart new file mode 100644 index 00000000..2a063ad9 --- /dev/null +++ b/packages/flutter_hooks/test/use_on_stream_change_test.dart @@ -0,0 +1,293 @@ +import 'dart:async'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; + +import 'mock.dart'; + +void main() { + testWidgets('debugFillProperties', (tester) async { + final stream = Stream.value(42); + + await tester.pumpWidget( + HookBuilder(builder: (context) { + useOnStreamChange(stream); + return const SizedBox(); + }), + ); + + await tester.pump(); + + final element = tester.element(find.byType(HookBuilder)); + + expect( + element + .toDiagnosticsNode(style: DiagnosticsTreeStyle.offstage) + .toStringDeep(), + equalsIgnoringHashCodes( + 'HookBuilder\n' + " │ useOnStreamChange: Instance of '_ControllerSubscription'\n" + ' └SizedBox(renderObject: RenderConstrainedBox#00000)\n', + ), + ); + }); + + testWidgets('calls onData when data arrives', (tester) async { + const data = 42; + final stream = Stream.value(data); + + late int value; + + await tester.pumpWidget( + HookBuilder(builder: (context) { + useOnStreamChange( + stream, + onData: (data) { + value = data; + }, + ); + return const SizedBox(); + }), + ); + + expect(value, data); + }); + + testWidgets('calls onError when error occurs', (tester) async { + final error = Exception(); + final stream = Stream.error(error); + + late Object receivedError; + + await tester.pumpWidget( + HookBuilder(builder: (context) { + useOnStreamChange( + stream, + onError: (error, stackTrace) { + receivedError = error; + }, + ); + return const SizedBox(); + }), + ); + + expect(receivedError, same(error)); + }); + + testWidgets('calls onDone when stream is closed', (tester) async { + final streamController = StreamController.broadcast(); + + var onDoneCalled = false; + + await tester.pumpWidget( + HookBuilder(builder: (context) { + useOnStreamChange( + streamController.stream, + onDone: () { + onDoneCalled = true; + }, + ); + return const SizedBox(); + }), + ); + + await streamController.close(); + + expect(onDoneCalled, isTrue); + }); + + testWidgets( + 'cancels subscription when cancelOnError is true and error occurrs', + (tester) => tester.runAsync(() async { + final streamController = StreamController(); + + await tester.pumpWidget( + HookBuilder(builder: (context) { + useOnStreamChange( + streamController.stream, + // onError needs to be set to prevent unhandled errors from propagating. + onError: (error, stackTrace) {}, + cancelOnError: true, + ); + return const SizedBox(); + }), + ); + + expect(streamController.hasListener, isTrue); + + streamController.addError(Exception()); + + await tester.pump(); + + expect(streamController.hasListener, isFalse); + + await streamController.close(); + }), + ); + + testWidgets( + 'listens new stream when stream is changed', + (tester) => tester.runAsync(() async { + final streamController1 = StreamController(); + final streamController2 = StreamController(); + + late StreamSubscription subscription1; + late StreamSubscription subscription2; + + await tester.pumpWidget( + HookBuilder( + key: const Key('hook_builder'), + builder: (context) { + subscription1 = useOnStreamChange(streamController1.stream)!; + return const SizedBox(); + }, + ), + ); + + expect(streamController1.hasListener, isTrue); + expect(streamController2.hasListener, isFalse); + + await tester.pumpWidget( + HookBuilder( + key: const Key('hook_builder'), + builder: (context) { + subscription2 = useOnStreamChange(streamController2.stream)!; + return const SizedBox(); + }, + ), + ); + + expect(streamController1.hasListener, isFalse); + expect(streamController2.hasListener, isTrue); + + expect(subscription1, isNot(same(subscription2))); + + await streamController1.close(); + await streamController2.close(); + }), + ); + + testWidgets( + 'resubscribes stream when cancelOnError changed', + (tester) => tester.runAsync(() async { + var listenCount = 0; + var cancelCount = 0; + + final streamController = StreamController.broadcast( + onListen: () => listenCount++, + onCancel: () => cancelCount++, + ); + final stream = streamController.stream; + + await tester.pumpWidget( + HookBuilder( + key: const Key('hook'), + builder: (context) { + useOnStreamChange( + stream, + cancelOnError: false, + ); + return const SizedBox(); + }, + ), + ); + + expect(listenCount, 1); + expect(cancelCount, isZero); + + await tester.pumpWidget( + HookBuilder( + key: const Key('hook'), + builder: (context) { + useOnStreamChange( + stream, + cancelOnError: true, + ); + return const SizedBox(); + }, + ), + ); + + expect(listenCount, 2); + expect(cancelCount, 1); + + await streamController.close(); + }), + ); + + testWidgets( + 'stop listening when cancel is called on StreamSubscription', + (tester) => tester.runAsync(() async { + final streamController = StreamController(); + + late StreamSubscription subscription; + + await tester.pumpWidget( + HookBuilder( + key: const Key('hook_builder'), + builder: (context) { + subscription = useOnStreamChange(streamController.stream)!; + return const SizedBox(); + }, + ), + ); + + await subscription.cancel(); + + expect(streamController.hasListener, isFalse); + + await streamController.close(); + }), + ); + + testWidgets('returns null when stream is null', (tester) async { + StreamSubscription? subscription; + + await tester.pumpWidget( + HookBuilder(builder: (context) { + subscription = useOnStreamChange(null); + return const SizedBox(); + }), + ); + + expect(subscription, isNull); + }); + + testWidgets( + 'unsubscribes when stream changed to null', + (tester) => tester.runAsync(() async { + final streamController = StreamController(); + + StreamSubscription? subscription; + + await tester.pumpWidget( + HookBuilder( + key: const Key('hook_builder'), + builder: (context) { + subscription = useOnStreamChange(streamController.stream); + return const SizedBox(); + }, + ), + ); + + expect(streamController.hasListener, isTrue); + expect(subscription, isNotNull); + + await tester.pumpWidget( + HookBuilder( + key: const Key('hook_builder'), + builder: (context) { + subscription = useOnStreamChange(null); + return const SizedBox(); + }, + ), + ); + + expect(streamController.hasListener, isFalse); + expect(subscription, isNotNull); + + await streamController.close(); + }), + ); +} From c23a57489dcb14ef130f9e92d2da04caf7ae7401 Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Fri, 21 Jul 2023 11:00:45 +0200 Subject: [PATCH 297/384] Changelog --- packages/flutter_hooks/CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/flutter_hooks/CHANGELOG.md b/packages/flutter_hooks/CHANGELOG.md index 366057d5..6acf7879 100644 --- a/packages/flutter_hooks/CHANGELOG.md +++ b/packages/flutter_hooks/CHANGELOG.md @@ -1,3 +1,7 @@ +## Unreleased minor + +- Added `useOnStreamChange` (thanks to @jezsung) + ## 0.19.0 - 2023-07-10 - Added `useSearchController` (thanks to @snapsl) From 9b8bf702c5e6d22d6c7c1d6fb460d1fffd1c7f53 Mon Sep 17 00:00:00 2001 From: Jaesung Hong Date: Tue, 25 Jul 2023 18:07:48 +0900 Subject: [PATCH 298/384] Mimic React Hook's dependencies comparison (#374) closes #155 This PR changes the comparison behavior to mimic the React Hook dependencies comparison which uses Object.is(). Previously, keys are compared by the == operator. The Dart == operator works differently from the JavaScript Object.is(): Object.is() Object.is(NaN, NaN) // true Object.is(0, -0) // false Dart == double.nan == double.nan // false 0 == -0 // true This PR checks if a list of keys contain a value with the type num and proceeds to handle cases where the value is either NaN or 0.0 and -0.0. --- packages/flutter_hooks/CHANGELOG.md | 5 +- packages/flutter_hooks/lib/src/framework.dart | 22 ++++++++- .../flutter_hooks/test/use_effect_test.dart | 49 +++++++++++++++++++ 3 files changed, 74 insertions(+), 2 deletions(-) diff --git a/packages/flutter_hooks/CHANGELOG.md b/packages/flutter_hooks/CHANGELOG.md index 6acf7879..0f47d765 100644 --- a/packages/flutter_hooks/CHANGELOG.md +++ b/packages/flutter_hooks/CHANGELOG.md @@ -1,5 +1,8 @@ -## Unreleased minor +## Unreleased major +- **Breaking**: `keys` comparison has changed under the following conditions: + - Change from `double.nan` to `double.nan` preserves the state. + - Change from `0.0` to `-0.0` or vice versa does NOT preserve the state. - Added `useOnStreamChange` (thanks to @jezsung) ## 0.19.0 - 2023-07-10 diff --git a/packages/flutter_hooks/lib/src/framework.dart b/packages/flutter_hooks/lib/src/framework.dart index d46bcd8f..d3104a9b 100644 --- a/packages/flutter_hooks/lib/src/framework.dart +++ b/packages/flutter_hooks/lib/src/framework.dart @@ -153,6 +153,10 @@ Calling them outside of build method leads to an unstable state and is therefore /// /// - `hook1.keys == hook2.keys` (typically if the list is immutable) /// - If there's any difference in the content of [Hook.keys], using `operator==`. + /// + /// There are exceptions when comparing [Hook.keys] before using `operator==`: + /// - A state is preserved when one of the [Hook.keys] is [double.nan]. + /// - A state is NOT preserved when one of the [Hook.keys] is changed from 0.0 to -0.0. static bool shouldPreserveState(Hook hook1, Hook hook2) { final p1 = hook1.keys; final p2 = hook2.keys; @@ -172,7 +176,23 @@ Calling them outside of build method leads to an unstable state and is therefore if (!i1.moveNext() || !i2.moveNext()) { return true; } - if (i1.current != i2.current) { + + final curr1 = i1.current; + final curr2 = i2.current; + + if (curr1 is num && curr2 is num) { + // Checks if both are NaN + if (curr1.isNaN && curr2.isNaN) { + return true; + } + + // Checks if one is 0.0 and the other is -0.0 + if (curr1 == 0 && curr2 == 0) { + return curr1.isNegative == curr2.isNegative; + } + } + + if (curr1 != curr2) { return false; } } diff --git a/packages/flutter_hooks/test/use_effect_test.dart b/packages/flutter_hooks/test/use_effect_test.dart index 66a76f4c..d3a0c430 100644 --- a/packages/flutter_hooks/test/use_effect_test.dart +++ b/packages/flutter_hooks/test/use_effect_test.dart @@ -249,6 +249,55 @@ void main() { verifyNoMoreInteractions(disposerA); verifyNoMoreInteractions(effect); }); + + testWidgets( + 'does NOT call effect when one of keys is NaN and others are same', + (tester) async { + parameters = [double.nan]; + await tester.pumpWidget(builder()); + + verifyInOrder([ + effect(), + unrelated(), + ]); + verifyNoMoreInteractions(effect); + + parameters = [double.nan]; + await tester.pumpWidget(builder()); + + verifyNoMoreInteractions(effect); + }); + + testWidgets( + 'calls effect when one of keys is changed from 0.0 to -0.0 and vice versa', + (tester) async { + parameters = [0.0]; + await tester.pumpWidget(builder()); + + verifyInOrder([ + effect(), + unrelated(), + ]); + verifyNoMoreInteractions(effect); + + parameters = [-0.0]; + await tester.pumpWidget(builder()); + + verifyInOrder([ + effect(), + unrelated(), + ]); + verifyNoMoreInteractions(effect); + + parameters = [0.0]; + await tester.pumpWidget(builder()); + + verifyInOrder([ + effect(), + unrelated(), + ]); + verifyNoMoreInteractions(effect); + }); } class MockEffect extends Mock { From 6c07838d4714fd0a894cbc2314e911e8a0ebeffe Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Tue, 25 Jul 2023 13:11:23 +0200 Subject: [PATCH 299/384] The following packages have been updated: flutter_hooks : 0.19.0 -> 0.20.0 --- packages/flutter_hooks/CHANGELOG.md | 2 +- packages/flutter_hooks/example/pubspec.yaml | 2 +- packages/flutter_hooks/pubspec.yaml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/flutter_hooks/CHANGELOG.md b/packages/flutter_hooks/CHANGELOG.md index 0f47d765..7d12a0d8 100644 --- a/packages/flutter_hooks/CHANGELOG.md +++ b/packages/flutter_hooks/CHANGELOG.md @@ -1,4 +1,4 @@ -## Unreleased major +## 0.20.0 - 2023-07-25 - **Breaking**: `keys` comparison has changed under the following conditions: - Change from `double.nan` to `double.nan` preserves the state. diff --git a/packages/flutter_hooks/example/pubspec.yaml b/packages/flutter_hooks/example/pubspec.yaml index 2ec419e9..fe06d81e 100644 --- a/packages/flutter_hooks/example/pubspec.yaml +++ b/packages/flutter_hooks/example/pubspec.yaml @@ -2,7 +2,7 @@ name: flutter_hooks_gallery description: A new Flutter project. publish_to: none -version: 1.0.0+1 +version: 1.0.1 environment: sdk: ">=2.12.0 <4.0.0" diff --git a/packages/flutter_hooks/pubspec.yaml b/packages/flutter_hooks/pubspec.yaml index b7151161..439bb583 100644 --- a/packages/flutter_hooks/pubspec.yaml +++ b/packages/flutter_hooks/pubspec.yaml @@ -3,7 +3,7 @@ description: A flutter implementation of React hooks. It adds a new kind of widg homepage: https://github.com/rrousselGit/flutter_hooks repository: https://github.com/rrousselGit/flutter_hooks/tree/master/packages/flutter_hooks issue_tracker: https://github.com/rrousselGit/flutter_hooks/issues -version: 0.19.0 +version: 0.20.0 environment: sdk: ">=2.17.0 <3.0.0" From 52ce7dc98ede8c47cf7f471d0adb59879ecb0af3 Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Sun, 13 Aug 2023 10:59:42 +0200 Subject: [PATCH 300/384] Fix test --- packages/flutter_hooks/test/mock.dart | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/flutter_hooks/test/mock.dart b/packages/flutter_hooks/test/mock.dart index f7aaab8a..6b68671e 100644 --- a/packages/flutter_hooks/test/mock.dart +++ b/packages/flutter_hooks/test/mock.dart @@ -97,9 +97,7 @@ Element _rootOf(Element element) { void hotReload(WidgetTester tester) { final root = _rootOf(tester.allElements.first); - TestWidgetsFlutterBinding.ensureInitialized() - .buildOwner - ?.reassemble(root, null); + TestWidgetsFlutterBinding.ensureInitialized().buildOwner?.reassemble(root); } class MockSetState extends Mock { From 72597be6651422e79ae939623dae0ac875b08128 Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Sun, 13 Aug 2023 11:04:13 +0200 Subject: [PATCH 301/384] Disable CI on master --- .github/workflows/build.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 3bbc433b..c8bfff11 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -16,7 +16,6 @@ jobs: package: - flutter_hooks channel: - - stable - master steps: From 0e12b1803b9b87d83a630babcca11af4a741ac31 Mon Sep 17 00:00:00 2001 From: Alexander Ivashkin Date: Tue, 29 Aug 2023 10:10:22 +0400 Subject: [PATCH 302/384] Feature: ability to use nullable listener in useListenableSelector (#377) --- .../lib/src/listenable_selector.dart | 12 +++---- .../test/use_listenable_selector_test.dart | 32 +++++++++++++++++++ 2 files changed, 38 insertions(+), 6 deletions(-) diff --git a/packages/flutter_hooks/lib/src/listenable_selector.dart b/packages/flutter_hooks/lib/src/listenable_selector.dart index c73becc4..0f0bf428 100644 --- a/packages/flutter_hooks/lib/src/listenable_selector.dart +++ b/packages/flutter_hooks/lib/src/listenable_selector.dart @@ -25,7 +25,7 @@ part of 'hooks.dart'; /// R useListenableSelector( - Listenable listenable, + Listenable? listenable, R Function() selector, ) { return use(_ListenableSelectorHook(listenable, selector)); @@ -34,7 +34,7 @@ R useListenableSelector( class _ListenableSelectorHook extends Hook { const _ListenableSelectorHook(this.listenable, this.selector); - final Listenable listenable; + final Listenable? listenable; final R Function() selector; @override @@ -49,7 +49,7 @@ class _ListenableSelectorHookState @override void initHook() { super.initHook(); - hook.listenable.addListener(_listener); + hook.listenable?.addListener(_listener); } @override @@ -63,8 +63,8 @@ class _ListenableSelectorHookState } if (hook.listenable != oldHook.listenable) { - oldHook.listenable.removeListener(_listener); - hook.listenable.addListener(_listener); + oldHook.listenable?.removeListener(_listener); + hook.listenable?.addListener(_listener); _selectorResult = hook.selector(); } } @@ -83,7 +83,7 @@ class _ListenableSelectorHookState @override void dispose() { - hook.listenable.removeListener(_listener); + hook.listenable?.removeListener(_listener); } @override diff --git a/packages/flutter_hooks/test/use_listenable_selector_test.dart b/packages/flutter_hooks/test/use_listenable_selector_test.dart index 433d7717..99b14991 100644 --- a/packages/flutter_hooks/test/use_listenable_selector_test.dart +++ b/packages/flutter_hooks/test/use_listenable_selector_test.dart @@ -85,6 +85,38 @@ void main() { listenable.dispose(); }); + testWidgets('null as Listener', (tester) async { + const notFoundValue = -1; + final testListener = ValueNotifier(false); + final listenable = ValueNotifier(777); + var result = 0; + + await tester.pumpWidget( + HookBuilder( + builder: (context) { + final shouldUseListener = useListenableSelector(testListener, () { + return testListener.value; + }); + + final actualListener = shouldUseListener ? listenable : null; + + result = useListenableSelector(actualListener, () { + return actualListener?.value ?? notFoundValue; + }); + + return Container(); + }, + ), + ); + + expect(result, notFoundValue); + testListener.value = true; + + await tester.pump(); + + expect(result, listenable.value); + }); + testWidgets('update selector', (tester) async { final listenable = ValueNotifier(0); var isOdd = false; From b434f9230a4c50f25ca92953aeea4cff8b6ce6ab Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Tue, 29 Aug 2023 08:12:05 +0200 Subject: [PATCH 303/384] flutter_hooks : 0.20.0 -> 0.20.1 --- packages/flutter_hooks/CHANGELOG.md | 4 ++++ packages/flutter_hooks/example/pubspec.yaml | 2 +- packages/flutter_hooks/pubspec.yaml | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/flutter_hooks/CHANGELOG.md b/packages/flutter_hooks/CHANGELOG.md index 7d12a0d8..89e31791 100644 --- a/packages/flutter_hooks/CHANGELOG.md +++ b/packages/flutter_hooks/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.20.1 - 2023-08-29 + +- `useListenableSelector` now supports `null` listeners (thanks to @JWo1F) + ## 0.20.0 - 2023-07-25 - **Breaking**: `keys` comparison has changed under the following conditions: diff --git a/packages/flutter_hooks/example/pubspec.yaml b/packages/flutter_hooks/example/pubspec.yaml index fe06d81e..57d6788f 100644 --- a/packages/flutter_hooks/example/pubspec.yaml +++ b/packages/flutter_hooks/example/pubspec.yaml @@ -2,7 +2,7 @@ name: flutter_hooks_gallery description: A new Flutter project. publish_to: none -version: 1.0.1 +version: 1.0.2 environment: sdk: ">=2.12.0 <4.0.0" diff --git a/packages/flutter_hooks/pubspec.yaml b/packages/flutter_hooks/pubspec.yaml index 439bb583..1de57bb7 100644 --- a/packages/flutter_hooks/pubspec.yaml +++ b/packages/flutter_hooks/pubspec.yaml @@ -3,7 +3,7 @@ description: A flutter implementation of React hooks. It adds a new kind of widg homepage: https://github.com/rrousselGit/flutter_hooks repository: https://github.com/rrousselGit/flutter_hooks/tree/master/packages/flutter_hooks issue_tracker: https://github.com/rrousselGit/flutter_hooks/issues -version: 0.20.0 +version: 0.20.1 environment: sdk: ">=2.17.0 <3.0.0" From abe0b508d3640b04ab202cab0a1cfabd2d911845 Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Mon, 2 Oct 2023 11:03:34 +0200 Subject: [PATCH 304/384] fixes #384 (#385) --- packages/flutter_hooks/CHANGELOG.md | 4 ++ packages/flutter_hooks/lib/src/framework.dart | 7 ++- .../flutter_hooks/test/use_effect_test.dart | 46 +++++++++++++++++++ 3 files changed, 55 insertions(+), 2 deletions(-) diff --git a/packages/flutter_hooks/CHANGELOG.md b/packages/flutter_hooks/CHANGELOG.md index 89e31791..6a4e95c8 100644 --- a/packages/flutter_hooks/CHANGELOG.md +++ b/packages/flutter_hooks/CHANGELOG.md @@ -1,3 +1,7 @@ +## Unreleased patch + +- Fixed key matching algorithm incorrectly not checking all keys if a 0 or a NaN is present (thanks to @AlexDochioiu) + ## 0.20.1 - 2023-08-29 - `useListenableSelector` now supports `null` listeners (thanks to @JWo1F) diff --git a/packages/flutter_hooks/lib/src/framework.dart b/packages/flutter_hooks/lib/src/framework.dart index d3104a9b..36ff6081 100644 --- a/packages/flutter_hooks/lib/src/framework.dart +++ b/packages/flutter_hooks/lib/src/framework.dart @@ -183,12 +183,15 @@ Calling them outside of build method leads to an unstable state and is therefore if (curr1 is num && curr2 is num) { // Checks if both are NaN if (curr1.isNaN && curr2.isNaN) { - return true; + continue; } // Checks if one is 0.0 and the other is -0.0 if (curr1 == 0 && curr2 == 0) { - return curr1.isNegative == curr2.isNegative; + if (curr1.isNegative != curr2.isNegative) { + return false; + } + continue; } } diff --git a/packages/flutter_hooks/test/use_effect_test.dart b/packages/flutter_hooks/test/use_effect_test.dart index d3a0c430..a6c42aa4 100644 --- a/packages/flutter_hooks/test/use_effect_test.dart +++ b/packages/flutter_hooks/test/use_effect_test.dart @@ -268,6 +268,29 @@ void main() { verifyNoMoreInteractions(effect); }); + testWidgets( + 'calls effect when first key is two matching NaN but second key is different', + (tester) async { + // Regression test for https://github.com/rrousselGit/flutter_hooks/issues/384 + parameters = [double.nan, 0]; + await tester.pumpWidget(builder()); + + verifyInOrder([ + effect(), + unrelated(), + ]); + verifyNoMoreInteractions(effect); + + parameters = [double.nan, 1]; + await tester.pumpWidget(builder()); + + verifyInOrder([ + effect(), + unrelated(), + ]); + verifyNoMoreInteractions(effect); + }); + testWidgets( 'calls effect when one of keys is changed from 0.0 to -0.0 and vice versa', (tester) async { @@ -298,6 +321,29 @@ void main() { ]); verifyNoMoreInteractions(effect); }); + + testWidgets( + 'calls effect when first key is two matching 0 with same sign, but second key is different', + (tester) async { + // Regression test for https://github.com/rrousselGit/flutter_hooks/issues/384 + parameters = [-0, 42]; + await tester.pumpWidget(builder()); + + verifyInOrder([ + effect(), + unrelated(), + ]); + verifyNoMoreInteractions(effect); + + parameters = [-0, 43]; + await tester.pumpWidget(builder()); + + verifyInOrder([ + effect(), + unrelated(), + ]); + verifyNoMoreInteractions(effect); + }); } class MockEffect extends Mock { From 2b89f55baae817464b69d30f3c187a9de0b71e9e Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Mon, 2 Oct 2023 11:08:21 +0200 Subject: [PATCH 305/384] flutter_hooks : 0.20.1 -> 0.20.2 --- packages/flutter_hooks/CHANGELOG.md | 2 +- packages/flutter_hooks/example/pubspec.yaml | 2 +- packages/flutter_hooks/pubspec.yaml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/flutter_hooks/CHANGELOG.md b/packages/flutter_hooks/CHANGELOG.md index 6a4e95c8..8015d275 100644 --- a/packages/flutter_hooks/CHANGELOG.md +++ b/packages/flutter_hooks/CHANGELOG.md @@ -1,4 +1,4 @@ -## Unreleased patch +## 0.20.2 - 2023-10-02 - Fixed key matching algorithm incorrectly not checking all keys if a 0 or a NaN is present (thanks to @AlexDochioiu) diff --git a/packages/flutter_hooks/example/pubspec.yaml b/packages/flutter_hooks/example/pubspec.yaml index 57d6788f..f696c85e 100644 --- a/packages/flutter_hooks/example/pubspec.yaml +++ b/packages/flutter_hooks/example/pubspec.yaml @@ -2,7 +2,7 @@ name: flutter_hooks_gallery description: A new Flutter project. publish_to: none -version: 1.0.2 +version: 1.0.3 environment: sdk: ">=2.12.0 <4.0.0" diff --git a/packages/flutter_hooks/pubspec.yaml b/packages/flutter_hooks/pubspec.yaml index 1de57bb7..f0ae7667 100644 --- a/packages/flutter_hooks/pubspec.yaml +++ b/packages/flutter_hooks/pubspec.yaml @@ -3,7 +3,7 @@ description: A flutter implementation of React hooks. It adds a new kind of widg homepage: https://github.com/rrousselGit/flutter_hooks repository: https://github.com/rrousselGit/flutter_hooks/tree/master/packages/flutter_hooks issue_tracker: https://github.com/rrousselGit/flutter_hooks/issues -version: 0.20.1 +version: 0.20.2 environment: sdk: ">=2.17.0 <3.0.0" From a40e7ccfdb61b08e6941808616a9432059215956 Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Tue, 10 Oct 2023 15:09:25 +0200 Subject: [PATCH 306/384] Update project.yml --- .github/workflows/project.yml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.github/workflows/project.yml b/.github/workflows/project.yml index 902ec6dc..d8b8abf1 100644 --- a/.github/workflows/project.yml +++ b/.github/workflows/project.yml @@ -5,10 +5,6 @@ on: types: - opened - reopened - pull_request: - types: - - opened - - reopened jobs: add-to-project: From e9e0caea2d7fc8a12c7d900f940afafcf02718c1 Mon Sep 17 00:00:00 2001 From: droidbg <41873024+droidbg@users.noreply.github.com> Date: Tue, 10 Oct 2023 18:40:55 +0530 Subject: [PATCH 307/384] [#375] Add. hook for ExpansionTileController (#386) --- README.md | 33 ++++---- .../lib/src/expansion_tile_controller.dart | 28 +++++++ packages/flutter_hooks/lib/src/hooks.dart | 3 +- .../use_expansion_tile_controller_test.dart | 82 +++++++++++++++++++ 4 files changed, 129 insertions(+), 17 deletions(-) create mode 100644 packages/flutter_hooks/lib/src/expansion_tile_controller.dart create mode 100644 packages/flutter_hooks/test/use_expansion_tile_controller_test.dart diff --git a/README.md b/README.md index 94fc208f..558dfe67 100644 --- a/README.md +++ b/README.md @@ -338,22 +338,23 @@ 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`. | +| 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`. | +| [useExpansionTileController](https://api.flutter.dev/flutter/material/ExpansionTileController-class.html) | Creates a `ExpansionTileController`. | ## Contributions diff --git a/packages/flutter_hooks/lib/src/expansion_tile_controller.dart b/packages/flutter_hooks/lib/src/expansion_tile_controller.dart new file mode 100644 index 00000000..f698f54b --- /dev/null +++ b/packages/flutter_hooks/lib/src/expansion_tile_controller.dart @@ -0,0 +1,28 @@ +part of 'hooks.dart'; + +/// Creates a [ExpansionTileController] that will be disposed automatically. +/// +/// See also: +/// - [ExpansionTileController] +ExpansionTileController useExpansionTileController({List? keys}) { + return use(_ExpansionTileControllerHook(keys: keys)); +} + +class _ExpansionTileControllerHook extends Hook { + const _ExpansionTileControllerHook({List? keys}) : super(keys: keys); + + @override + HookState> + createState() => _ExpansionTileControllerHookState(); +} + +class _ExpansionTileControllerHookState + extends HookState { + final controller = ExpansionTileController(); + + @override + String get debugLabel => 'useExpansionTileController'; + + @override + ExpansionTileController build(BuildContext context) => controller; +} diff --git a/packages/flutter_hooks/lib/src/hooks.dart b/packages/flutter_hooks/lib/src/hooks.dart index 036289fd..d23ebabb 100644 --- a/packages/flutter_hooks/lib/src/hooks.dart +++ b/packages/flutter_hooks/lib/src/hooks.dart @@ -2,7 +2,7 @@ import 'dart:async'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart' - show Brightness, SearchController, TabController; + show Brightness, ExpansionTileController, SearchController, TabController; import 'package:flutter/scheduler.dart'; import 'package:flutter/widgets.dart'; @@ -10,6 +10,7 @@ import 'framework.dart'; part 'animation.dart'; part 'async.dart'; +part 'expansion_tile_controller.dart'; part 'focus_node.dart'; part 'focus_scope_node.dart'; part 'keep_alive.dart'; diff --git a/packages/flutter_hooks/test/use_expansion_tile_controller_test.dart b/packages/flutter_hooks/test/use_expansion_tile_controller_test.dart new file mode 100644 index 00000000..84dd57d1 --- /dev/null +++ b/packages/flutter_hooks/test/use_expansion_tile_controller_test.dart @@ -0,0 +1,82 @@ +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) { + useExpansionTileController(); + return const SizedBox(); + }), + ); + + await tester.pump(); + + final element = tester.element(find.byType(HookBuilder)); + + expect( + element + .toDiagnosticsNode(style: DiagnosticsTreeStyle.offstage) + .toStringDeep(), + equalsIgnoringHashCodes( + 'HookBuilder\n' + " │ useExpansionTileController: Instance of 'ExpansionTileController'\n" + ' └SizedBox(renderObject: RenderConstrainedBox#00000)\n', + ), + ); + }); + + group('useExpansionTileController', () { + testWidgets('initial values matches with real constructor', (tester) async { + late ExpansionTileController controller; + final controller2 = ExpansionTileController(); + + await tester.pumpWidget(MaterialApp( + home: Scaffold( + body: HookBuilder(builder: (context) { + controller = useExpansionTileController(); + return Column( + children: [ + ExpansionTile( + controller: controller, + title: const Text('Expansion Tile'), + ), + ExpansionTile( + controller: controller2, + title: const Text('Expansion Tile 2'), + ), + ], + ); + }), + ), + )); + expect(controller, isA()); + expect(controller.isExpanded, controller2.isExpanded); + }); + + testWidgets('check expansion/collapse of tile', (tester) async { + late ExpansionTileController controller; + await tester.pumpWidget(MaterialApp( + home: Scaffold( + body: HookBuilder(builder: (context) { + controller = useExpansionTileController(); + return ExpansionTile( + controller: controller, + title: const Text('Expansion Tile'), + ); + }), + ), + )); + + expect(controller.isExpanded, false); + controller.expand(); + expect(controller.isExpanded, true); + controller.collapse(); + expect(controller.isExpanded, false); + }); + }); +} From b2520ffc3e96757ed502b764eaf7b1f46ea7723c Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Tue, 10 Oct 2023 15:11:57 +0200 Subject: [PATCH 308/384] Changelog --- packages/flutter_hooks/CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/flutter_hooks/CHANGELOG.md b/packages/flutter_hooks/CHANGELOG.md index 8015d275..57da8daa 100644 --- a/packages/flutter_hooks/CHANGELOG.md +++ b/packages/flutter_hooks/CHANGELOG.md @@ -1,3 +1,7 @@ +## Unreleased patch + +- Added `useExpansionTileController` (thanks to @droidbg) + ## 0.20.2 - 2023-10-02 - Fixed key matching algorithm incorrectly not checking all keys if a 0 or a NaN is present (thanks to @AlexDochioiu) From 1589dd0be254b09fbe3b4672f4a9e0a1059f10d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Havl=C3=AD=C4=8Dek?= <50631920+AdamHavlicek@users.noreply.github.com> Date: Tue, 10 Oct 2023 15:22:45 +0200 Subject: [PATCH 309/384] feat(hook): implemented hook for MaterialStateController (#383) --- README.md | 35 +++--- packages/flutter_hooks/lib/src/hooks.dart | 9 +- .../lib/src/material_states_controller.dart | 44 +++++++ .../use_material_states_controller_test.dart | 113 ++++++++++++++++++ 4 files changed, 183 insertions(+), 18 deletions(-) create mode 100644 packages/flutter_hooks/lib/src/material_states_controller.dart create mode 100644 packages/flutter_hooks/test/use_material_states_controller_test.dart diff --git a/README.md b/README.md index 558dfe67..e5b90bf1 100644 --- a/README.md +++ b/README.md @@ -338,23 +338,24 @@ 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`. | -| [useExpansionTileController](https://api.flutter.dev/flutter/material/ExpansionTileController-class.html) | Creates a `ExpansionTileController`. | +| 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`. | +| [useMaterialStatesController](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useMaterialStatesController.html) | Creates and disposes a `MaterialStatesController`. | +| [useExpansionTileController](https://api.flutter.dev/flutter/material/ExpansionTileController-class.html) | Creates a `ExpansionTileController`. | ## Contributions diff --git a/packages/flutter_hooks/lib/src/hooks.dart b/packages/flutter_hooks/lib/src/hooks.dart index d23ebabb..206426e0 100644 --- a/packages/flutter_hooks/lib/src/hooks.dart +++ b/packages/flutter_hooks/lib/src/hooks.dart @@ -2,7 +2,13 @@ import 'dart:async'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart' - show Brightness, ExpansionTileController, SearchController, TabController; + show + Brightness, + ExpansionTileController, + MaterialState, + MaterialStatesController, + SearchController, + TabController; import 'package:flutter/scheduler.dart'; import 'package:flutter/widgets.dart'; @@ -26,3 +32,4 @@ part 'tab_controller.dart'; part 'text_controller.dart'; part 'transformation_controller.dart'; part 'widgets_binding_observer.dart'; +part 'material_states_controller.dart'; diff --git a/packages/flutter_hooks/lib/src/material_states_controller.dart b/packages/flutter_hooks/lib/src/material_states_controller.dart new file mode 100644 index 00000000..45aee2e2 --- /dev/null +++ b/packages/flutter_hooks/lib/src/material_states_controller.dart @@ -0,0 +1,44 @@ +part of 'hooks.dart'; + +/// Creates a [MaterialStatesController] that will be disposed automatically. +/// +/// See also: +/// - [MaterialStatesController] +MaterialStatesController useMaterialStatesController({ + Set? values, + List? keys, +}) { + return use( + _MaterialStatesControllerHook( + values: values, + keys: keys, + ), + ); +} + +class _MaterialStatesControllerHook extends Hook { + const _MaterialStatesControllerHook({ + required this.values, + super.keys, + }); + + final Set? values; + + @override + HookState> + createState() => _MaterialStateControllerHookState(); +} + +class _MaterialStateControllerHookState + extends HookState { + late final controller = MaterialStatesController(hook.values); + + @override + MaterialStatesController build(BuildContext context) => controller; + + @override + void dispose() => controller.dispose(); + + @override + String get debugLabel => 'useMaterialStatesController'; +} diff --git a/packages/flutter_hooks/test/use_material_states_controller_test.dart b/packages/flutter_hooks/test/use_material_states_controller_test.dart new file mode 100644 index 00000000..d30e6275 --- /dev/null +++ b/packages/flutter_hooks/test/use_material_states_controller_test.dart @@ -0,0 +1,113 @@ +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) { + useMaterialStatesController(); + return const SizedBox(); + }), + ); + + final element = tester.element(find.byType(HookBuilder)); + + expect( + element + .toDiagnosticsNode(style: DiagnosticsTreeStyle.offstage) + .toStringDeep(), + equalsIgnoringHashCodes( + 'HookBuilder\n' + ' │ useMaterialStatesController: MaterialStatesController#00000({})\n' + ' └SizedBox(renderObject: RenderConstrainedBox#00000)\n', + ), + ); + }); + + group('useMaterialStatesController', () { + testWidgets('initial values matches with real constructor', (tester) async { + late MaterialStatesController controller; + late MaterialStatesController controller2; + + await tester.pumpWidget( + HookBuilder(builder: (context) { + controller2 = MaterialStatesController(); + controller = useMaterialStatesController(); + return Container(); + }), + ); + + expect(controller.value, controller2.value); + }); + testWidgets("returns a MaterialStatesController that doesn't change", + (tester) async { + late MaterialStatesController controller; + late MaterialStatesController controller2; + + await tester.pumpWidget( + HookBuilder(builder: (context) { + controller = useMaterialStatesController(); + return Container(); + }), + ); + + expect(controller, isA()); + + await tester.pumpWidget( + HookBuilder(builder: (context) { + controller2 = useMaterialStatesController(); + return Container(); + }), + ); + + expect(identical(controller, controller2), isTrue); + }); + + testWidgets('passes hook parameters to the MaterialStatesController', + (tester) async { + late MaterialStatesController controller; + + await tester.pumpWidget( + HookBuilder( + builder: (context) { + controller = useMaterialStatesController( + values: {MaterialState.selected}, + ); + + return Container(); + }, + ), + ); + + expect(controller.value, {MaterialState.selected}); + }); + + testWidgets('disposes the MaterialStatesController on unmount', + (tester) async { + late MaterialStatesController controller; + + await tester.pumpWidget( + HookBuilder( + builder: (context) { + controller = useMaterialStatesController(); + 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'))), + ); + }); + }); +} From 999e9850d5efd70f8ec325908726d64e5155a66c Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Tue, 10 Oct 2023 15:28:13 +0200 Subject: [PATCH 310/384] Changelog --- packages/flutter_hooks/CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/flutter_hooks/CHANGELOG.md b/packages/flutter_hooks/CHANGELOG.md index 57da8daa..44fe39a0 100644 --- a/packages/flutter_hooks/CHANGELOG.md +++ b/packages/flutter_hooks/CHANGELOG.md @@ -1,6 +1,7 @@ ## Unreleased patch - Added `useExpansionTileController` (thanks to @droidbg) +- Added `useMaterialStateController` (thanks to @AdamHavlicek) ## 0.20.2 - 2023-10-02 From c0d705e223319737b070fd789d43466d46036548 Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Tue, 10 Oct 2023 15:31:30 +0200 Subject: [PATCH 311/384] flutter_hooks : 0.20.2 -> 0.20.3 --- packages/flutter_hooks/CHANGELOG.md | 2 +- packages/flutter_hooks/pubspec.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/flutter_hooks/CHANGELOG.md b/packages/flutter_hooks/CHANGELOG.md index 44fe39a0..ad6c11fc 100644 --- a/packages/flutter_hooks/CHANGELOG.md +++ b/packages/flutter_hooks/CHANGELOG.md @@ -1,4 +1,4 @@ -## Unreleased patch +## 0.20.3 - 2023-10-10 - Added `useExpansionTileController` (thanks to @droidbg) - Added `useMaterialStateController` (thanks to @AdamHavlicek) diff --git a/packages/flutter_hooks/pubspec.yaml b/packages/flutter_hooks/pubspec.yaml index f0ae7667..c2cbce47 100644 --- a/packages/flutter_hooks/pubspec.yaml +++ b/packages/flutter_hooks/pubspec.yaml @@ -3,7 +3,7 @@ description: A flutter implementation of React hooks. It adds a new kind of widg homepage: https://github.com/rrousselGit/flutter_hooks repository: https://github.com/rrousselGit/flutter_hooks/tree/master/packages/flutter_hooks issue_tracker: https://github.com/rrousselGit/flutter_hooks/issues -version: 0.20.2 +version: 0.20.3 environment: sdk: ">=2.17.0 <3.0.0" From 8ff38636e5bac135910d922efa31af9f4964b2e0 Mon Sep 17 00:00:00 2001 From: Cierra_Runis <2864283875@qq.com> Date: Thu, 19 Oct 2023 22:29:45 +0800 Subject: [PATCH 312/384] Add Readme for zh_cn (#388) --- README.md | 2 +- .../resources/translations/ko_kr/README.md | 2 +- .../resources/translations/pt_br/README.md | 2 +- .../resources/translations/zh_cn/README.md | 379 ++++++++++++++++++ 4 files changed, 382 insertions(+), 3 deletions(-) create mode 100644 packages/flutter_hooks/resources/translations/zh_cn/README.md diff --git a/README.md b/README.md index e5b90bf1..2eed934c 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -[English](https://github.com/rrousselGit/flutter_hooks/blob/master/README.md) | [Português](https://github.com/rrousselGit/flutter_hooks/blob/master/packages/flutter_hooks/resources/translations/pt_br/README.md) | [한국어](https://github.com/rrousselGit/flutter_hooks/blob/master/packages/flutter_hooks/resources/translations/ko_kr/README.md) +[English](https://github.com/rrousselGit/flutter_hooks/blob/master/README.md) | [Português](https://github.com/rrousselGit/flutter_hooks/blob/master/packages/flutter_hooks/resources/translations/pt_br/README.md) | [한국어](https://github.com/rrousselGit/flutter_hooks/blob/master/packages/flutter_hooks/resources/translations/ko_kr/README.md) | [简体中文](https://github.com/rrousselGit/flutter_hooks/blob/master/packages/flutter_hooks/resources/translations/zh_cn/README.md) [![Build](https://github.com/rrousselGit/flutter_hooks/workflows/Build/badge.svg)](https://github.com/rrousselGit/flutter_hooks/actions?query=workflow%3ABuild) [![codecov](https://codecov.io/gh/rrousselGit/flutter_hooks/branch/master/graph/badge.svg)](https://codecov.io/gh/rrousselGit/flutter_hooks) [![pub package](https://img.shields.io/pub/v/flutter_hooks.svg)](https://pub.dev/packages/flutter_hooks) [![pub package](https://img.shields.io/badge/Awesome-Flutter-blue.svg?longCache=true&style=flat-square)](https://github.com/Solido/awesome-flutter) Discord diff --git a/packages/flutter_hooks/resources/translations/ko_kr/README.md b/packages/flutter_hooks/resources/translations/ko_kr/README.md index 1f7c3952..e32ba14f 100644 --- a/packages/flutter_hooks/resources/translations/ko_kr/README.md +++ b/packages/flutter_hooks/resources/translations/ko_kr/README.md @@ -1,4 +1,4 @@ -[English](https://github.com/rrousselGit/flutter_hooks/blob/master/README.md) | [Português](https://github.com/rrousselGit/flutter_hooks/blob/master/packages/flutter_hooks/resources/translations/pt_br/README.md) | [한국어](https://github.com/rrousselGit/flutter_hooks/blob/master/packages/flutter_hooks/resources/translations/ko_kr/README.md) +[English](https://github.com/rrousselGit/flutter_hooks/blob/master/README.md) | [Português](https://github.com/rrousselGit/flutter_hooks/blob/master/packages/flutter_hooks/resources/translations/pt_br/README.md) | [한국어](https://github.com/rrousselGit/flutter_hooks/blob/master/packages/flutter_hooks/resources/translations/ko_kr/README.md) | [简体中文](https://github.com/rrousselGit/flutter_hooks/blob/master/packages/flutter_hooks/resources/translations/zh_cn/README.md) [![Build](https://github.com/rrousselGit/flutter_hooks/workflows/Build/badge.svg)](https://github.com/rrousselGit/flutter_hooks/actions?query=workflow%3ABuild) [![codecov](https://codecov.io/gh/rrousselGit/flutter_hooks/branch/master/graph/badge.svg)](https://codecov.io/gh/rrousselGit/flutter_hooks) [![pub package](https://img.shields.io/pub/v/flutter_hooks.svg)](https://pub.dartlang.org/packages/flutter_hooks) [![pub package](https://img.shields.io/badge/Awesome-Flutter-blue.svg?longCache=true&style=flat-square)](https://github.com/Solido/awesome-flutter) Discord diff --git a/packages/flutter_hooks/resources/translations/pt_br/README.md b/packages/flutter_hooks/resources/translations/pt_br/README.md index 65f68158..60b787c6 100644 --- a/packages/flutter_hooks/resources/translations/pt_br/README.md +++ b/packages/flutter_hooks/resources/translations/pt_br/README.md @@ -1,4 +1,4 @@ -[English](https://github.com/rrousselGit/flutter_hooks/blob/master/README.md) | [Português](https://github.com/rrousselGit/flutter_hooks/blob/master/packages/flutter_hooks/resources/translations/pt_br/README.md) | [한국어](https://github.com/rrousselGit/flutter_hooks/blob/master/packages/flutter_hooks/resources/translations/ko_kr/README.md) +[English](https://github.com/rrousselGit/flutter_hooks/blob/master/README.md) | [Português](https://github.com/rrousselGit/flutter_hooks/blob/master/packages/flutter_hooks/resources/translations/pt_br/README.md) | [한국어](https://github.com/rrousselGit/flutter_hooks/blob/master/packages/flutter_hooks/resources/translations/ko_kr/README.md) | [简体中文](https://github.com/rrousselGit/flutter_hooks/blob/master/packages/flutter_hooks/resources/translations/zh_cn/README.md) [![Build Status](https://travis-ci.org/rrousselGit/flutter_hooks.svg?branch=master)](https://travis-ci.org/rrousselGit/flutter_hooks) [![codecov](https://codecov.io/gh/rrousselGit/flutter_hooks/branch/master/graph/badge.svg)](https://codecov.io/gh/rrousselGit/flutter_hooks) [![pub package](https://img.shields.io/pub/v/flutter_hooks.svg)](https://pub.dartlang.org/packages/flutter_hooks) [![pub package](https://img.shields.io/badge/Awesome-Flutter-blue.svg?longCache=true&style=flat-square)](https://github.com/Solido/awesome-flutter) diff --git a/packages/flutter_hooks/resources/translations/zh_cn/README.md b/packages/flutter_hooks/resources/translations/zh_cn/README.md new file mode 100644 index 00000000..b656baae --- /dev/null +++ b/packages/flutter_hooks/resources/translations/zh_cn/README.md @@ -0,0 +1,379 @@ +[English](https://github.com/rrousselGit/flutter_hooks/blob/master/README.md) | [Português](https://github.com/rrousselGit/flutter_hooks/blob/master/packages/flutter_hooks/resources/translations/pt_br/README.md) | [한국어](https://github.com/rrousselGit/flutter_hooks/blob/master/packages/flutter_hooks/resources/translations/ko_kr/README.md) | [简体中文](https://github.com/rrousselGit/flutter_hooks/blob/master/packages/flutter_hooks/resources/translations/zh_cn/README.md) + +[![Build](https://github.com/rrousselGit/flutter_hooks/workflows/Build/badge.svg)](https://github.com/rrousselGit/flutter_hooks/actions?query=workflow%3ABuild) [![codecov](https://codecov.io/gh/rrousselGit/flutter_hooks/branch/master/graph/badge.svg)](https://codecov.io/gh/rrousselGit/flutter_hooks) [![pub package](https://img.shields.io/pub/v/flutter_hooks.svg)](https://pub.dev/packages/flutter_hooks) [![pub package](https://img.shields.io/badge/Awesome-Flutter-blue.svg?longCache=true&style=flat-square)](https://github.com/Solido/awesome-flutter) + +Discord + +

+ +

+ +# Flutter Hooks + +一个 React 钩子在 Flutter 上的实现:[Making Sense of React Hooks](https://medium.com/@dan_abramov/making-sense-of-react-hooks-fdbde8803889) + +钩子是一种用来管理 `Widget` 生命周期的新对象,以减少重复代码、增加组件间复用性。 + +## 动机 + +`StatefulWidget` 有个大问题,它很难减少 `initState` 或 `dispose` 的调用,一个简明的例子就是 `AnimationController`: + +```dart +class Example extends StatefulWidget { + final Duration duration; + + const Example({Key? key, required this.duration}) + : super(key: key); + + @override + _ExampleState createState() => _ExampleState(); +} + +class _ExampleState extends State with SingleTickerProviderStateMixin { + AnimationController? _controller; + + @override + void initState() { + super.initState(); + _controller = AnimationController(vsync: this, duration: widget.duration); + } + + @override + void didUpdateWidget(Example oldWidget) { + super.didUpdateWidget(oldWidget); + if (widget.duration != oldWidget.duration) { + _controller!.duration = widget.duration; + } + } + + @override + void dispose() { + _controller!.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Container(); + } +} +``` + +所有想要使用 `AnimationController` 的组件都几乎必须从头开始重新实现这些逻辑,这当然不是我们想要的。 + +Dart 的 mixin 能部分解决这个问题,但随之又有其它问题: + +- 一个给定的 mixin 只能被一个类使用一次 +- Mixin 和类共用一个对象\ + 这意味着如果两个 mixin 用一个变量名分别定义自己的变量,结果要么是编译失败,要么行为诡异。 + +--- + +这个库提供了另一个解决方法: + +```dart +class Example extends HookWidget { + const Example({Key? key, required this.duration}) + : super(key: key); + + final Duration duration; + + @override + Widget build(BuildContext context) { + final controller = useAnimationController(duration: duration); + return Container(); + } +} +``` + +这段代码和之前的例子有一样的功能。\ +它仍然会 dispose `AnimationController`,并在 `Example.duration` 改变时更新它的 `duration`。 + +但猜你在想: + +> 那些逻辑都哪去了? + +那些逻辑都已经被移入了 `useAnimationController` 里,这是这个库直接带有的(见 [已有的钩子](https://github.com/Cierra-Runis/flutter_hooks/blob/master/packages/flutter_hooks/resources/translations/zh_cn/README.md#%E5%B7%B2%E6%9C%89%E7%9A%84%E9%92%A9%E5%AD%90) )——这就是我们所说的 _钩子_。 + +钩子是一种有着如下部分特性的新对象: + +- 只能在混入了 `Hooks` 的组件的 `build` 方法内使用 +- 同类的钩子能复用任意多次\ + 如下的代码定义了两个独立的 `AnimationController`,并且都在组件重建时被正确的保留 + + ```dart + Widget build(BuildContext context) { + final controller = useAnimationController(); + final controller2 = useAnimationController(); + return Container(); + } + ``` + +- 钩子和其它钩子与组件完全独立\ + 这说明他们能被很简单的抽离到一个包并发布到 [pub](https://pub.dev/) 上去给其他人用 + +## 原理 + +与 `State` 类似,钩子被存在 `Widget` 的 `Element` 里。但和存个 `State` 不一样,`Element` 存的是 `List`。\ +再就是想要使用 `Hook` 的话,就必须调用 `Hook.use`。 + +由 `use` 返回的钩子由其被调用的次数决定。\ +第一次调用返回第一个钩子,第二次返回第二个,第三次返回第三个这样。 + +如果还是不太能理解的话,钩子的一个雏形可能长下面这样: + +```dart +class HookElement extends Element { + List _hooks; + int _hookIndex; + + T use(Hook hook) => _hooks[_hookIndex++].build(this); + + @override + performRebuild() { + _hookIndex = 0; + super.performRebuild(); + } +} +``` + +想要更多有关钩子是怎么实现的解释的话,这里有篇讲钩子在 React 是怎么实现的挺不错的 [文章](https://medium.com/@ryardley/react-hooks-not-magic-just-arrays-cd4f1857236e)。 + +## 约定 + +由于钩子由它们的 index 保留,有些约定是必须要遵守的: + +### _要_ 一直使用 `use` 作为你钩子的前缀 + +```dart +Widget build(BuildContext context) { + // 以 `use` 开头,非常好名字 + useMyHook(); + // 不以 `use` 开头,会让人以为这不是一个钩子 + myHook(); + // .... +} +``` + +### _要_ 直接调用钩子 + +```dart +Widget build(BuildContext context) { + useMyHook(); + // .... +} +``` + +### _不要_ 将 `use` 包到条件语句里 + +```dart +Widget build(BuildContext context) { + if (condition) { + useMyHook(); + } + // .... +} +``` + +--- + +### 有关热重载 + +由于钩子由它们的 index 保留,可能有人认为在重构时热重载会搞崩程序。 + +冇问题的,为了能使用钩子,`HookWidget` 覆写了默认的热重载行为,但还有一些情况下钩子的状态会被重置。 + +设想如下三个钩子: + +```dart +useA(); +useB(0); +useC(); +``` + +接下来我们在热重载后修改 `HookB` 的参数: + +```dart +useA(); +useB(42); +useC(); +``` + +那么一切正常,所有的钩子都保留了他们的状态。 + +现在再删掉 `HookB` 试试: + +```dart +useA(); +useC(); +``` + +在这种情况下,`HookA` 会保留它的状态,但 `HookC` 会被强制重置。\ +这是因为重构并热重载后,在第一个被影响的钩子 _之后_ 的所有钩子都会被 dispose 掉。\ +因此,由于 `HookC` 在 `HookB` _之后_,所以它会被 dispose 掉。 + +## 如何创建钩子 + +这有两种方法: + +- 函数式钩子 + + 函数是目前用来写钩子的最常用方法。\ + 多亏钩子能被自然的组合,一个函数就能将其他的钩子组合为一个复杂的自定义钩子。\ + 而且我们约定好了这些函数都以 `use` 为前缀。 + + 如下代码构建了一个自定义钩子,其创建了一个变量,并在变量改变时在终端显示日志。 + + ```dart + ValueNotifier useLoggedState([T initialData]) { + final result = useState(initialData); + useValueChanged(result.value, (_, __) { + print(result.value); + }); + return result; + } + ``` + +- 类钩子 + + 当一个钩子变得过于复杂时,可以将其转化为一个继承 `Hook` 的类——然后就能拿来调用 `Hook.use`。\ + 作为一个类,钩子看起来和 `State` 类差不多,有着组件的生命周期和方法,比如 `initHook`、`dispose`和`setState`。 + + 而且一个好的实践是将类藏在一个函数后面: + + ```dart + Result useMyHook() { + return use(const _TimeAlive()); + } + ``` + + 如下代码构建了一个自定义钩子,其能在其被 dispose 时打印其状态存在的总时长。 + + ```dart + class _TimeAlive extends Hook { + const _TimeAlive(); + + @override + _TimeAliveState createState() => _TimeAliveState(); + } + + class _TimeAliveState extends HookState { + DateTime start; + + @override + void initHook() { + super.initHook(); + start = DateTime.now(); + } + + @override + void build(BuildContext context) {} + + @override + void dispose() { + print(DateTime.now().difference(start)); + super.dispose(); + } + } + ``` + +## 已有的钩子 + +Flutter_Hooks 已经包含一些不同类别的可复用的钩子: + +### 基础类别 + +与组件不同生命周期交互的低级钩子。 + +| 名称 | 介绍 | +| -------------------------------------------------------------------------------------------------------- | ------------------------------------------- | +| [useEffect](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useEffect.html) | 对副作用很有用,可以选择取消它们 | +| [useState](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useState.html) | 创建并订阅一个变量 | +| [useMemoized](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useMemoized.html) | 缓存复杂对象的实例 | +| [useRef](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useRef.html) | 创建一个包含单个可变属性的对象 | +| [useCallback](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useCallback.html) | 缓存一个函数的实例 | +| [useContext](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useContext.html) | 包含构建中的 `HookWidget` 的 `BuildContext` | +| [useValueChanged](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useValueChanged.html) | 监听一个值并在其改变时触发回调 | + +### 绑定对象 + +这类钩子用以操作现有的 Flutter 及 Dart 对象。\ +它们负责创建、更新以及 dispose 对象。 + +#### dart:async 相关 + +| 名称 | 介绍 | +| ---------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------- | +| [useStream](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useStream.html) | 订阅一个 `Stream`,并以 `AsyncSnapshot` 返回它目前的状态 | +| [useStreamController](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useStreamController.html) | 创建一个会自动 dispose 的 `StreamController` | +| [useOnStreamChange](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useOnStreamChange.html) | 订阅一个 `Stream`,注册处理函数,返回 `StreamSubscription` | +| [useFuture](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useFuture.html) | 订阅一个 `Future` 并以 `AsyncSnapshot` 返回它目前的状态 | + +#### Animation 相关 + +| 名称 | 介绍 | +| ------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------- | +| [useSingleTickerProvider](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useSingleTickerProvider.html) | 创建单一用途的 `TickerProvider` | +| [useAnimationController](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useAnimationController.html) | 创建一个会自动 dispose 的 `AnimationController` | +| [useAnimation](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useAnimation.html) | 订阅一个 `Animation` 并返回它的值 | + +#### Listenable 相关 + +| 名称 | 介绍 | +| -------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------- | +| [useListenable](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useListenable.html) | 订阅一个 `Listenable` 并在 listener 调用时将组件标脏 | +| [useListenableSelector](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useListenableSelector.html) | 和 `useListenable` 类似,但支持过滤 UI 重建 | +| [useValueNotifier](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useValueNotifier.html) | 创建一个会自动 dispose 的 `ValueNotifier` | +| [useValueListenable](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useValueListenable.html) | 订阅一个 `ValueListenable` 并返回它的值 | + +#### 杂项 + +一组无明确主题的钩子。 + +| 名称 | 介绍 | +| ------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------- | +| [useReducer](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useReducer.html) | 对于更复杂的状态,用以替代 `useState` | +| [usePrevious](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/usePrevious.html) | 返回调用 `usePrevious` 的上一个参数 | +| [useTextEditingController](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useTextEditingController-constant.html) | 创建一个 `TextEditingController` | +| [useFocusNode](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useFocusNode.html) | 创建一个 `FocusNode` | +| [useTabController](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useTabController.html) | 创建并自动 dispose 一个 `TabController` | +| [useScrollController](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useScrollController.html) | 创建并自动 dispose 一个 `ScrollController` | +| [usePageController](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/usePageController.html) | 创建并自动 dispose 一个 `PageController` | +| [useAppLifecycleState](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useAppLifecycleState.html) | 返回当前的 `AppLifecycleState`,并在改变时重建组件 | +| [useOnAppLifecycleStateChange](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useOnAppLifecycleStateChange.html) | 监听 `AppLifecycleState` 并在其改变时触发回调 | +| [useTransformationController](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useTransformationController.html) | 创建并自动 dispose 一个 `TransformationController` | +| [useIsMounted](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useIsMounted.html) | 对钩子而言和 `State.mounted` 一样 | +| [useAutomaticKeepAlive](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useAutomaticKeepAlive.html) | 对钩子而言和 `AutomaticKeepAlive` 一样 | +| [useOnPlatformBrightnessChange](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useOnPlatformBrightnessChange.html) | 监听平台 `Brightness` 并在其改变时触发回调 | +| [useMaterialStatesController](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useMaterialStatesController.html) | 创建并自动 dispose 一个 `MaterialStatesController` | +| [useExpansionTileController](https://api.flutter.dev/flutter/material/ExpansionTileController-class.html) | 创建一个 `ExpansionTileController` | + +## 贡献 + +欢迎贡献! + +如果你觉得少了某个钩子,别多想直接开个 Pull Request ~ + +为了合并新的自定义钩子,你需要按如下规则办事: + +- 介绍使用例 + + 开个 issue 解释一下为什么我们需要这个钩子,怎么用它……\ + 这很重要,如果这个钩子对很多人没有吸引力,那么它就不会被合并。 + + 如果你被拒了也没关系!这并不意味着以后也被拒绝,如果越来越多的人感兴趣。\ + 在这之前,你也可以把你的钩子发布到 [pub](https://pub.dev/) 上~ + +- 为你的钩子写测试 + + 除非钩子被完全测试好,不然不会合并,以防未来不经意破坏了它也没法发现。 + +- 把它加到 README 并写介绍 + +## 赞助 + +

+ + + +

From 9bc9fe0fb1a881bc63ebc5a2ad1367d66ba996f8 Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Mon, 30 Oct 2023 11:44:01 +0100 Subject: [PATCH 313/384] Update useMemoized docs fixes #391 --- packages/flutter_hooks/lib/src/primitives.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/flutter_hooks/lib/src/primitives.dart b/packages/flutter_hooks/lib/src/primitives.dart index 31e742c0..70ba0d90 100644 --- a/packages/flutter_hooks/lib/src/primitives.dart +++ b/packages/flutter_hooks/lib/src/primitives.dart @@ -53,7 +53,7 @@ T useCallback( /// [useMemoized] will immediately call [valueBuilder] on first call and store its result. /// Later, when the [HookWidget] rebuilds, the call to [useMemoized] will return the previously created instance without calling [valueBuilder]. /// -/// A subsequent call of [useMemoized] with different [keys] will call [useMemoized] again to create a new instance. +/// A subsequent call of [useMemoized] with different [keys] will re-invoke the function to create a new instance. T useMemoized( T Function() valueBuilder, [ List keys = const [], From f7daeb3d75c71e988a9f105905c41db36846776e Mon Sep 17 00:00:00 2001 From: Deep145757 <146447579+Deep145757@users.noreply.github.com> Date: Mon, 30 Oct 2023 18:28:28 +0530 Subject: [PATCH 314/384] doc(README): remove typo (#392) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 2eed934c..cdb8a10d 100644 --- a/README.md +++ b/README.md @@ -143,7 +143,7 @@ class HookElement extends Element { ``` For more explanation of how hooks are implemented, here's a great article about -how is was done in React: https://medium.com/@ryardley/react-hooks-not-magic-just-arrays-cd4f1857236e +how it was done in React: https://medium.com/@ryardley/react-hooks-not-magic-just-arrays-cd4f1857236e ## Rules From 46ff844a3155fe6345e761373461774ff10a76ee Mon Sep 17 00:00:00 2001 From: itisnajim Date: Tue, 5 Dec 2023 11:28:32 +0100 Subject: [PATCH 315/384] Add hook useDebounced (#395) --- README.md | 1 + packages/flutter_hooks/CHANGELOG.md | 4 + packages/flutter_hooks/lib/src/debounced.dart | 88 +++++++++++++++++++ packages/flutter_hooks/lib/src/hooks.dart | 1 + .../flutter_hooks/test/use_debounce_test.dart | 86 ++++++++++++++++++ 5 files changed, 180 insertions(+) create mode 100644 packages/flutter_hooks/lib/src/debounced.dart create mode 100644 packages/flutter_hooks/test/use_debounce_test.dart diff --git a/README.md b/README.md index cdb8a10d..6fb701fd 100644 --- a/README.md +++ b/README.md @@ -356,6 +356,7 @@ A series of hooks with no particular theme. | [useSearchController](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useSearchController.html) | Creates and disposes a `SearchController`. | | [useMaterialStatesController](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useMaterialStatesController.html) | Creates and disposes a `MaterialStatesController`. | | [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 | ## Contributions diff --git a/packages/flutter_hooks/CHANGELOG.md b/packages/flutter_hooks/CHANGELOG.md index ad6c11fc..93cc4f5b 100644 --- a/packages/flutter_hooks/CHANGELOG.md +++ b/packages/flutter_hooks/CHANGELOG.md @@ -1,3 +1,7 @@ +## Unreleased fix + +- Added `useDebounced` (thanks to @itisnajim) + ## 0.20.3 - 2023-10-10 - Added `useExpansionTileController` (thanks to @droidbg) diff --git a/packages/flutter_hooks/lib/src/debounced.dart b/packages/flutter_hooks/lib/src/debounced.dart new file mode 100644 index 00000000..f93ccd48 --- /dev/null +++ b/packages/flutter_hooks/lib/src/debounced.dart @@ -0,0 +1,88 @@ +part of 'hooks.dart'; + +/// Returns a debounced version of the provided value [toDebounce], triggering +/// widget updates accordingly after a specified [timeout] duration. +/// +/// Example: +/// ```dart +/// String userInput = ''; // Your input value +/// +/// // Create a debounced version of userInput +/// final debouncedInput = useDebounced( +/// userInput, +/// Duration(milliseconds: 500), // Set your desired timeout +/// ); +/// // Assume a fetch method fetchData(String query) exists +/// useEffect(() { +/// fetchData(debouncedInput); // Use debouncedInput as a dependency +/// return null; +/// }, [debouncedInput]); +/// ``` +T? useDebounced( + T toDebounce, + Duration timeout, +) { + return use( + _DebouncedHook( + toDebounce: toDebounce, + timeout: timeout, + ), + ); +} + +class _DebouncedHook extends Hook { + const _DebouncedHook({ + required this.toDebounce, + required this.timeout, + }); + + final T toDebounce; + final Duration timeout; + + @override + _DebouncedHookState createState() => _DebouncedHookState(); +} + +class _DebouncedHookState extends HookState> { + T? _state; + Timer? _timer; + + @override + void initHook() { + super.initHook(); + _startDebounce(hook.toDebounce); + } + + void _startDebounce(T toDebounce) { + _timer?.cancel(); + _timer = Timer(hook.timeout, () { + setState(() { + _state = toDebounce; + }); + }); + } + + @override + void didUpdateHook(_DebouncedHook oldHook) { + if (hook.toDebounce != oldHook.toDebounce || + hook.timeout != oldHook.timeout) { + _startDebounce(hook.toDebounce); + } + } + + @override + T? build(BuildContext context) => _state; + + @override + Object? get debugValue => _state; + + @override + String get debugLabel => 'useDebounced<$T>'; + + @override + void dispose() { + _timer?.cancel(); + _timer = null; + super.dispose(); + } +} diff --git a/packages/flutter_hooks/lib/src/hooks.dart b/packages/flutter_hooks/lib/src/hooks.dart index 206426e0..7b6cfbfb 100644 --- a/packages/flutter_hooks/lib/src/hooks.dart +++ b/packages/flutter_hooks/lib/src/hooks.dart @@ -33,3 +33,4 @@ part 'text_controller.dart'; part 'transformation_controller.dart'; part 'widgets_binding_observer.dart'; part 'material_states_controller.dart'; +part 'debounced.dart'; diff --git a/packages/flutter_hooks/test/use_debounce_test.dart b/packages/flutter_hooks/test/use_debounce_test.dart new file mode 100644 index 00000000..7cd4a811 --- /dev/null +++ b/packages/flutter_hooks/test/use_debounce_test.dart @@ -0,0 +1,86 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + testWidgets('debugFillProperties', (tester) async { + await tester.pumpWidget( + HookBuilder(builder: (context) { + useDebounced(42, const Duration(milliseconds: 500)); + return const SizedBox(); + }), + ); + + // await the debouncer timeout. + await tester.pumpAndSettle(const Duration(milliseconds: 500)); + + final element = tester.element(find.byType(HookBuilder)); + + expect( + element + .toDiagnosticsNode(style: DiagnosticsTreeStyle.offstage) + .toStringDeep(), + equalsIgnoringHashCodes( + 'HookBuilder\n' + ' │ useDebounced: 42\n' + ' └SizedBox(renderObject: RenderConstrainedBox#00000)\n', + ), + ); + }); + + group('useDebounced', () { + testWidgets('default value is null', (tester) async { + await tester.pumpWidget( + HookBuilder(builder: (context) { + final debounced = useDebounced( + 'test', + const Duration(milliseconds: 500), + ); + return Text( + debounced.toString(), + textDirection: TextDirection.ltr, + ); + }), + ); + expect(find.text('null'), findsOneWidget); + }); + testWidgets('basic', (tester) async { + await tester.pumpWidget( + HookBuilder( + builder: (context) { + final textValueNotifier = useState('Hello'); + final debounced = useDebounced( + textValueNotifier.value, + const Duration(milliseconds: 500), + ); + + useEffect(() { + textValueNotifier.value = 'World'; + return null; + }, [textValueNotifier.value]); + + return Text( + debounced.toString(), + textDirection: TextDirection.ltr, + ); + }, + ), + ); + + // Ensure that the initial value displayed is 'null' + expect(find.text('null'), findsOneWidget); + + // Ensure that after a 500ms delay, the value 'Hello' of 'textValueNotifier' + // is reflected in 'debounced' and displayed + await tester.pumpAndSettle(const Duration(milliseconds: 500)); + expect(find.text('Hello'), findsOneWidget); + + // Ensure that after another 500ms delay, the value 'World' assigned to + // 'textValueNotifier' in the useEffect is reflected in 'debounced' + // and displayed + await tester.pumpAndSettle(const Duration(milliseconds: 500)); + expect(find.text('World'), findsOneWidget); + }); + }); +} From e8905f9eac2fbcd68d614dd0289bac59b3165dd9 Mon Sep 17 00:00:00 2001 From: Youngsup Oh Date: Mon, 11 Dec 2023 17:01:06 +0900 Subject: [PATCH 316/384] Update README.md (#400) Fix markdown formatting to render correctly --- packages/flutter_hooks/resources/translations/ko_kr/README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/flutter_hooks/resources/translations/ko_kr/README.md b/packages/flutter_hooks/resources/translations/ko_kr/README.md index e32ba14f..2ae0f869 100644 --- a/packages/flutter_hooks/resources/translations/ko_kr/README.md +++ b/packages/flutter_hooks/resources/translations/ko_kr/README.md @@ -210,6 +210,7 @@ useC(); 훅을 생성하기위한 두가지 방법이 있습니다: - 함수 + 함수는 훅을 작성하는 가장 일반적인 방법입니다. 훅이 자연스럽게 합성 가능한 덕분에, 함수는 다른 훅을 결합하여 더 복잡한 커스텀 훅을 만들 수 있습니다. 관례상, 이러한 함수는 `use`로 시작됩니다. 아래의 코드는 변수를 생성하고, 값이 변경될 때마다 콘솔에 로그를 남기는 커스텀 훅을 정의합니다: From e3e3440caea84cdb651683d008a27f8e3327f884 Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Fri, 29 Dec 2023 14:57:21 +0100 Subject: [PATCH 317/384] Remove visibleForTesting --- packages/flutter_hooks/CHANGELOG.md | 1 + packages/flutter_hooks/lib/src/framework.dart | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/flutter_hooks/CHANGELOG.md b/packages/flutter_hooks/CHANGELOG.md index 93cc4f5b..d3599bb7 100644 --- a/packages/flutter_hooks/CHANGELOG.md +++ b/packages/flutter_hooks/CHANGELOG.md @@ -1,6 +1,7 @@ ## Unreleased fix - Added `useDebounced` (thanks to @itisnajim) +- Removed `@visibleForTesting` on `HookMixin` ## 0.20.3 - 2023-10-10 diff --git a/packages/flutter_hooks/lib/src/framework.dart b/packages/flutter_hooks/lib/src/framework.dart index 36ff6081..1ba423a0 100644 --- a/packages/flutter_hooks/lib/src/framework.dart +++ b/packages/flutter_hooks/lib/src/framework.dart @@ -369,7 +369,6 @@ extension on HookElement { } /// An [Element] that uses a [HookWidget] as its configuration. -@visibleForTesting mixin HookElement on ComponentElement { static HookElement? _currentHookElement; From e861adfbe2b34c080476ea4b59327548743764e6 Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Fri, 29 Dec 2023 14:57:49 +0100 Subject: [PATCH 318/384] flutter_hooks : 0.20.3 -> 0.20.4 --- packages/flutter_hooks/CHANGELOG.md | 2 +- packages/flutter_hooks/example/pubspec.yaml | 2 +- packages/flutter_hooks/pubspec.yaml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/flutter_hooks/CHANGELOG.md b/packages/flutter_hooks/CHANGELOG.md index d3599bb7..6d00c1d8 100644 --- a/packages/flutter_hooks/CHANGELOG.md +++ b/packages/flutter_hooks/CHANGELOG.md @@ -1,4 +1,4 @@ -## Unreleased fix +## 0.20.4 - 2023-12-29 - Added `useDebounced` (thanks to @itisnajim) - Removed `@visibleForTesting` on `HookMixin` diff --git a/packages/flutter_hooks/example/pubspec.yaml b/packages/flutter_hooks/example/pubspec.yaml index f696c85e..a26696df 100644 --- a/packages/flutter_hooks/example/pubspec.yaml +++ b/packages/flutter_hooks/example/pubspec.yaml @@ -2,7 +2,7 @@ name: flutter_hooks_gallery description: A new Flutter project. publish_to: none -version: 1.0.3 +version: 1.0.4 environment: sdk: ">=2.12.0 <4.0.0" diff --git a/packages/flutter_hooks/pubspec.yaml b/packages/flutter_hooks/pubspec.yaml index c2cbce47..8f22ee53 100644 --- a/packages/flutter_hooks/pubspec.yaml +++ b/packages/flutter_hooks/pubspec.yaml @@ -3,7 +3,7 @@ description: A flutter implementation of React hooks. It adds a new kind of widg homepage: https://github.com/rrousselGit/flutter_hooks repository: https://github.com/rrousselGit/flutter_hooks/tree/master/packages/flutter_hooks issue_tracker: https://github.com/rrousselGit/flutter_hooks/issues -version: 0.20.3 +version: 0.20.4 environment: sdk: ">=2.17.0 <3.0.0" From cb127c4b88fa9337f309e828dba0b7b182721e51 Mon Sep 17 00:00:00 2001 From: R <9603168+Region40@users.noreply.github.com> Date: Thu, 4 Jan 2024 14:26:15 -0500 Subject: [PATCH 319/384] Clarify the use of useFuture (#408) --- packages/flutter_hooks/lib/src/async.dart | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/packages/flutter_hooks/lib/src/async.dart b/packages/flutter_hooks/lib/src/async.dart index 92ed39d8..8510c736 100644 --- a/packages/flutter_hooks/lib/src/async.dart +++ b/packages/flutter_hooks/lib/src/async.dart @@ -5,6 +5,20 @@ part of 'hooks.dart'; /// * [preserveState] determines if the current value should be preserved when changing /// the [Future] instance. /// +/// The [Future] needs to be created outside of [useFuture]. +/// If the [Future] is created inside [useFuture], then, every time the build +/// method gets called, the [Future] will be called again. One way to create +/// the [Future] outside of [useFuture] is by using [useMemoized]. +/// +/// ```dart +/// // BAD +/// useFuture(fetchFromDatabase()); +/// +/// // GOOD +/// final result = useMemoized(() => fetchFromDatabase()); +/// useFuture(result); +/// ``` +/// /// See also: /// * [Future], the listened object. /// * [useStream], similar to [useFuture] but for [Stream]. From 167484c98865365115d93a4eec7deeb4eae4cf95 Mon Sep 17 00:00:00 2001 From: Benjamin Date: Wed, 17 Jan 2024 10:22:37 -0500 Subject: [PATCH 320/384] Deprecate useIsMounted (#412) --- packages/flutter_hooks/CHANGELOG.md | 6 ++- packages/flutter_hooks/lib/src/misc.dart | 10 +++++ .../flutter_hooks/test/is_mounted_test.dart | 45 ------------------- 3 files changed, 15 insertions(+), 46 deletions(-) delete mode 100644 packages/flutter_hooks/test/is_mounted_test.dart diff --git a/packages/flutter_hooks/CHANGELOG.md b/packages/flutter_hooks/CHANGELOG.md index 6d00c1d8..8e7abb52 100644 --- a/packages/flutter_hooks/CHANGELOG.md +++ b/packages/flutter_hooks/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.21.0 - unreleased + +- Deprecate the `useIsMounted` hook as you should use `BuildContext.mounted` instead if you're on Flutter 3.7.0 or greater + ## 0.20.4 - 2023-12-29 - Added `useDebounced` (thanks to @itisnajim) @@ -134,7 +138,7 @@ Migrated flutter_hooks to null-safety (special thanks to @DevNico for the help!) **Breaking change**: -- Removed `HookState.didBuild`. +- Removed `HookState.didBuild`. If you still need it, use `addPostFrameCallback` or `Future.microtask`. **Non-breaking changes**: diff --git a/packages/flutter_hooks/lib/src/misc.dart b/packages/flutter_hooks/lib/src/misc.dart index 6f036a7d..147b7651 100644 --- a/packages/flutter_hooks/lib/src/misc.dart +++ b/packages/flutter_hooks/lib/src/misc.dart @@ -184,10 +184,14 @@ class _ReassembleHookState extends HookState { /// /// See also: /// * The [State.mounted] property. +@Deprecated( + "Use BuildContext.mounted instead if you're on Flutter 3.7.0 or greater", +) IsMounted useIsMounted() { return use(const _IsMountedHook()); } +// ignore: deprecated_member_use_from_same_package class _IsMountedHook extends Hook { const _IsMountedHook(); @@ -195,10 +199,12 @@ class _IsMountedHook extends Hook { _IsMountedHookState createState() => _IsMountedHookState(); } +// ignore: deprecated_member_use_from_same_package class _IsMountedHookState extends HookState { bool _mounted = true; @override + // ignore: deprecated_member_use_from_same_package IsMounted build(BuildContext context) => _isMounted; bool _isMounted() => _mounted; @@ -216,6 +222,10 @@ class _IsMountedHookState extends HookState { Object? get debugValue => _mounted; } +// ignore: deprecated_member_use_from_same_package /// Used by [useIsMounted] to allow widgets to determine if the [Widget] is still /// in the widget tree or not. +@Deprecated( + "Use BuildContext.mounted instead if you're on Flutter 3.7.0 or greater", +) typedef IsMounted = bool Function(); diff --git a/packages/flutter_hooks/test/is_mounted_test.dart b/packages/flutter_hooks/test/is_mounted_test.dart deleted file mode 100644 index 70f30270..00000000 --- a/packages/flutter_hooks/test/is_mounted_test.dart +++ /dev/null @@ -1,45 +0,0 @@ -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:flutter_test/flutter_test.dart'; - -void main() { - testWidgets('useIsMounted', (tester) async { - late IsMounted isMounted; - - await tester.pumpWidget(HookBuilder( - builder: (context) { - isMounted = useIsMounted(); - return Container(); - }, - )); - - expect(isMounted(), true); - - await tester.pumpWidget(Container()); - - expect(isMounted(), false); - }); - - testWidgets('debugFillProperties', (tester) async { - await tester.pumpWidget( - HookBuilder(builder: (context) { - useIsMounted(); - return const SizedBox(); - }), - ); - - final element = tester.element(find.byType(HookBuilder)); - - expect( - element - .toDiagnosticsNode(style: DiagnosticsTreeStyle.offstage) - .toStringDeep(), - equalsIgnoringHashCodes( - 'HookBuilder\n' - ' │ useIsMounted: true\n' - ' └SizedBox(renderObject: RenderConstrainedBox#00000)\n', - ), - ); - }); -} From 6c7e7f50dc05a1c205815f732d8ab228c3b67573 Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Mon, 5 Feb 2024 08:08:41 +0100 Subject: [PATCH 321/384] flutter_hooks : 0.20.4 -> 0.20.5 --- packages/flutter_hooks/CHANGELOG.md | 2 +- packages/flutter_hooks/example/pubspec.yaml | 1 - packages/flutter_hooks/pubspec.yaml | 2 +- 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/flutter_hooks/CHANGELOG.md b/packages/flutter_hooks/CHANGELOG.md index 8e7abb52..3f29f42b 100644 --- a/packages/flutter_hooks/CHANGELOG.md +++ b/packages/flutter_hooks/CHANGELOG.md @@ -1,4 +1,4 @@ -## 0.21.0 - unreleased +## 0.20.5 - 2024-02-05 - Deprecate the `useIsMounted` hook as you should use `BuildContext.mounted` instead if you're on Flutter 3.7.0 or greater diff --git a/packages/flutter_hooks/example/pubspec.yaml b/packages/flutter_hooks/example/pubspec.yaml index a26696df..73e80f22 100644 --- a/packages/flutter_hooks/example/pubspec.yaml +++ b/packages/flutter_hooks/example/pubspec.yaml @@ -2,7 +2,6 @@ name: flutter_hooks_gallery description: A new Flutter project. publish_to: none -version: 1.0.4 environment: sdk: ">=2.12.0 <4.0.0" diff --git a/packages/flutter_hooks/pubspec.yaml b/packages/flutter_hooks/pubspec.yaml index 8f22ee53..380ca760 100644 --- a/packages/flutter_hooks/pubspec.yaml +++ b/packages/flutter_hooks/pubspec.yaml @@ -3,7 +3,7 @@ description: A flutter implementation of React hooks. It adds a new kind of widg homepage: https://github.com/rrousselGit/flutter_hooks repository: https://github.com/rrousselGit/flutter_hooks/tree/master/packages/flutter_hooks issue_tracker: https://github.com/rrousselGit/flutter_hooks/issues -version: 0.20.4 +version: 0.20.5 environment: sdk: ">=2.17.0 <3.0.0" From c1a3f17f74d16dcc0621aba813155f102829eef6 Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Fri, 22 Mar 2024 11:59:27 +0100 Subject: [PATCH 322/384] Fix depreciation (#423) --- README.md | 2 +- packages/flutter_hooks/CHANGELOG.md | 6 ++- .../example/lib/star_wars/planet_screen.dart | 2 +- .../flutter_hooks/lib/src/focus_node.dart | 6 --- .../lib/src/focus_scope_node.dart | 6 --- packages/flutter_hooks/lib/src/hooks.dart | 8 ++-- .../lib/src/material_states_controller.dart | 44 ------------------- .../lib/src/widget_states_controller.dart | 44 +++++++++++++++++++ packages/flutter_hooks/pubspec.yaml | 2 +- .../resources/translations/zh_cn/README.md | 2 +- .../test/use_focus_node_test.dart | 14 ------ .../test/use_focus_scope_node_test.dart | 14 ------ .../use_material_states_controller_test.dart | 42 +++++++++--------- 13 files changed, 78 insertions(+), 114 deletions(-) delete mode 100644 packages/flutter_hooks/lib/src/material_states_controller.dart create mode 100644 packages/flutter_hooks/lib/src/widget_states_controller.dart diff --git a/README.md b/README.md index 6fb701fd..424d118d 100644 --- a/README.md +++ b/README.md @@ -354,7 +354,7 @@ A series of hooks with no particular theme. | [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`. | -| [useMaterialStatesController](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useMaterialStatesController.html) | Creates and disposes a `MaterialStatesController`. | +| [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 | diff --git a/packages/flutter_hooks/CHANGELOG.md b/packages/flutter_hooks/CHANGELOG.md index 3f29f42b..571233f3 100644 --- a/packages/flutter_hooks/CHANGELOG.md +++ b/packages/flutter_hooks/CHANGELOG.md @@ -1,3 +1,7 @@ +## Unreleased minor + +- Renamed `useMaterialStatesController` to `useWidgetStatesController` to follow the rename in Flutter. + ## 0.20.5 - 2024-02-05 - Deprecate the `useIsMounted` hook as you should use `BuildContext.mounted` instead if you're on Flutter 3.7.0 or greater @@ -10,7 +14,7 @@ ## 0.20.3 - 2023-10-10 - Added `useExpansionTileController` (thanks to @droidbg) -- Added `useMaterialStateController` (thanks to @AdamHavlicek) +- Added `useMaterialStatesController` (thanks to @AdamHavlicek) ## 0.20.2 - 2023-10-02 diff --git a/packages/flutter_hooks/example/lib/star_wars/planet_screen.dart b/packages/flutter_hooks/example/lib/star_wars/planet_screen.dart index 90852d6e..0640b582 100644 --- a/packages/flutter_hooks/example/lib/star_wars/planet_screen.dart +++ b/packages/flutter_hooks/example/lib/star_wars/planet_screen.dart @@ -107,7 +107,7 @@ class _Error extends StatelessWidget { if (errorMsg != null) Text(errorMsg!), ElevatedButton( style: ButtonStyle( - backgroundColor: MaterialStateProperty.all(Colors.redAccent), + backgroundColor: WidgetStateProperty.all(Colors.redAccent), ), onPressed: () async { await Provider.of<_PlanetHandler>( diff --git a/packages/flutter_hooks/lib/src/focus_node.dart b/packages/flutter_hooks/lib/src/focus_node.dart index 5b22582e..5dfb3e06 100644 --- a/packages/flutter_hooks/lib/src/focus_node.dart +++ b/packages/flutter_hooks/lib/src/focus_node.dart @@ -6,7 +6,6 @@ part of 'hooks.dart'; /// - [FocusNode] FocusNode useFocusNode({ String? debugLabel, - FocusOnKeyCallback? onKey, FocusOnKeyEventCallback? onKeyEvent, bool skipTraversal = false, bool canRequestFocus = true, @@ -15,7 +14,6 @@ FocusNode useFocusNode({ return use( _FocusNodeHook( debugLabel: debugLabel, - onKey: onKey, onKeyEvent: onKeyEvent, skipTraversal: skipTraversal, canRequestFocus: canRequestFocus, @@ -27,7 +25,6 @@ FocusNode useFocusNode({ class _FocusNodeHook extends Hook { const _FocusNodeHook({ this.debugLabel, - this.onKey, this.onKeyEvent, required this.skipTraversal, required this.canRequestFocus, @@ -35,7 +32,6 @@ class _FocusNodeHook extends Hook { }); final String? debugLabel; - final FocusOnKeyCallback? onKey; final FocusOnKeyEventCallback? onKeyEvent; final bool skipTraversal; final bool canRequestFocus; @@ -50,7 +46,6 @@ class _FocusNodeHook extends Hook { class _FocusNodeHookState extends HookState { late final FocusNode _focusNode = FocusNode( debugLabel: hook.debugLabel, - onKey: hook.onKey, onKeyEvent: hook.onKeyEvent, skipTraversal: hook.skipTraversal, canRequestFocus: hook.canRequestFocus, @@ -64,7 +59,6 @@ class _FocusNodeHookState extends HookState { ..skipTraversal = hook.skipTraversal ..canRequestFocus = hook.canRequestFocus ..descendantsAreFocusable = hook.descendantsAreFocusable - ..onKey = hook.onKey ..onKeyEvent = hook.onKeyEvent; } diff --git a/packages/flutter_hooks/lib/src/focus_scope_node.dart b/packages/flutter_hooks/lib/src/focus_scope_node.dart index 297a1bb0..8e1f84c4 100644 --- a/packages/flutter_hooks/lib/src/focus_scope_node.dart +++ b/packages/flutter_hooks/lib/src/focus_scope_node.dart @@ -6,7 +6,6 @@ part of 'hooks.dart'; /// - [FocusScopeNode] FocusScopeNode useFocusScopeNode({ String? debugLabel, - FocusOnKeyCallback? onKey, FocusOnKeyEventCallback? onKeyEvent, bool skipTraversal = false, bool canRequestFocus = true, @@ -14,7 +13,6 @@ FocusScopeNode useFocusScopeNode({ return use( _FocusScopeNodeHook( debugLabel: debugLabel, - onKey: onKey, onKeyEvent: onKeyEvent, skipTraversal: skipTraversal, canRequestFocus: canRequestFocus, @@ -25,14 +23,12 @@ FocusScopeNode useFocusScopeNode({ class _FocusScopeNodeHook extends Hook { const _FocusScopeNodeHook({ this.debugLabel, - this.onKey, this.onKeyEvent, required this.skipTraversal, required this.canRequestFocus, }); final String? debugLabel; - final FocusOnKeyCallback? onKey; final FocusOnKeyEventCallback? onKeyEvent; final bool skipTraversal; final bool canRequestFocus; @@ -47,7 +43,6 @@ class _FocusScopeNodeHookState extends HookState { late final FocusScopeNode _focusScopeNode = FocusScopeNode( debugLabel: hook.debugLabel, - onKey: hook.onKey, onKeyEvent: hook.onKeyEvent, skipTraversal: hook.skipTraversal, canRequestFocus: hook.canRequestFocus, @@ -59,7 +54,6 @@ class _FocusScopeNodeHookState ..debugLabel = hook.debugLabel ..skipTraversal = hook.skipTraversal ..canRequestFocus = hook.canRequestFocus - ..onKey = hook.onKey ..onKeyEvent = hook.onKeyEvent; } diff --git a/packages/flutter_hooks/lib/src/hooks.dart b/packages/flutter_hooks/lib/src/hooks.dart index 7b6cfbfb..859df563 100644 --- a/packages/flutter_hooks/lib/src/hooks.dart +++ b/packages/flutter_hooks/lib/src/hooks.dart @@ -5,10 +5,10 @@ import 'package:flutter/material.dart' show Brightness, ExpansionTileController, - MaterialState, - MaterialStatesController, + WidgetStatesController, SearchController, - TabController; + TabController, + WidgetState; import 'package:flutter/scheduler.dart'; import 'package:flutter/widgets.dart'; @@ -32,5 +32,5 @@ part 'tab_controller.dart'; part 'text_controller.dart'; part 'transformation_controller.dart'; part 'widgets_binding_observer.dart'; -part 'material_states_controller.dart'; +part 'widget_states_controller.dart'; part 'debounced.dart'; diff --git a/packages/flutter_hooks/lib/src/material_states_controller.dart b/packages/flutter_hooks/lib/src/material_states_controller.dart deleted file mode 100644 index 45aee2e2..00000000 --- a/packages/flutter_hooks/lib/src/material_states_controller.dart +++ /dev/null @@ -1,44 +0,0 @@ -part of 'hooks.dart'; - -/// Creates a [MaterialStatesController] that will be disposed automatically. -/// -/// See also: -/// - [MaterialStatesController] -MaterialStatesController useMaterialStatesController({ - Set? values, - List? keys, -}) { - return use( - _MaterialStatesControllerHook( - values: values, - keys: keys, - ), - ); -} - -class _MaterialStatesControllerHook extends Hook { - const _MaterialStatesControllerHook({ - required this.values, - super.keys, - }); - - final Set? values; - - @override - HookState> - createState() => _MaterialStateControllerHookState(); -} - -class _MaterialStateControllerHookState - extends HookState { - late final controller = MaterialStatesController(hook.values); - - @override - MaterialStatesController build(BuildContext context) => controller; - - @override - void dispose() => controller.dispose(); - - @override - String get debugLabel => 'useMaterialStatesController'; -} diff --git a/packages/flutter_hooks/lib/src/widget_states_controller.dart b/packages/flutter_hooks/lib/src/widget_states_controller.dart new file mode 100644 index 00000000..eab0ea2b --- /dev/null +++ b/packages/flutter_hooks/lib/src/widget_states_controller.dart @@ -0,0 +1,44 @@ +part of 'hooks.dart'; + +/// Creates a [WidgetStatesController] that will be disposed automatically. +/// +/// See also: +/// - [WidgetStatesController] +WidgetStatesController useWidgetStatesController({ + Set? values, + List? keys, +}) { + return use( + _WidgetStatesControllerHook( + values: values, + keys: keys, + ), + ); +} + +class _WidgetStatesControllerHook extends Hook { + const _WidgetStatesControllerHook({ + required this.values, + super.keys, + }); + + final Set? values; + + @override + HookState> + createState() => _WidgetStateControllerHookState(); +} + +class _WidgetStateControllerHookState + extends HookState { + late final controller = WidgetStatesController(hook.values); + + @override + WidgetStatesController build(BuildContext context) => controller; + + @override + void dispose() => controller.dispose(); + + @override + String get debugLabel => 'useWidgetStatesController'; +} diff --git a/packages/flutter_hooks/pubspec.yaml b/packages/flutter_hooks/pubspec.yaml index 380ca760..f5faba97 100644 --- a/packages/flutter_hooks/pubspec.yaml +++ b/packages/flutter_hooks/pubspec.yaml @@ -7,7 +7,7 @@ version: 0.20.5 environment: sdk: ">=2.17.0 <3.0.0" - flutter: ">=3.0.0" + flutter: ">=3.19.0-0.3.pre" dependencies: flutter: diff --git a/packages/flutter_hooks/resources/translations/zh_cn/README.md b/packages/flutter_hooks/resources/translations/zh_cn/README.md index b656baae..ebd3caf3 100644 --- a/packages/flutter_hooks/resources/translations/zh_cn/README.md +++ b/packages/flutter_hooks/resources/translations/zh_cn/README.md @@ -345,7 +345,7 @@ Flutter_Hooks 已经包含一些不同类别的可复用的钩子: | [useIsMounted](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useIsMounted.html) | 对钩子而言和 `State.mounted` 一样 | | [useAutomaticKeepAlive](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useAutomaticKeepAlive.html) | 对钩子而言和 `AutomaticKeepAlive` 一样 | | [useOnPlatformBrightnessChange](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useOnPlatformBrightnessChange.html) | 监听平台 `Brightness` 并在其改变时触发回调 | -| [useMaterialStatesController](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useMaterialStatesController.html) | 创建并自动 dispose 一个 `MaterialStatesController` | +| [useWidgetStatesController](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useWidgetStatesController.html) | 创建并自动 dispose 一个 `WidgetStatesController` | | [useExpansionTileController](https://api.flutter.dev/flutter/material/ExpansionTileController-class.html) | 创建一个 `ExpansionTileController` | ## 贡献 diff --git a/packages/flutter_hooks/test/use_focus_node_test.dart b/packages/flutter_hooks/test/use_focus_node_test.dart index 91642c66..055893ec 100644 --- a/packages/flutter_hooks/test/use_focus_node_test.dart +++ b/packages/flutter_hooks/test/use_focus_node_test.dart @@ -73,16 +73,12 @@ void main() { ); expect(focusNode.debugLabel, official.debugLabel); - expect(focusNode.onKey, official.onKey); expect(focusNode.skipTraversal, official.skipTraversal); expect(focusNode.canRequestFocus, official.canRequestFocus); expect(focusNode.descendantsAreFocusable, official.descendantsAreFocusable); }); testWidgets('has all the FocusNode parameters', (tester) async { - KeyEventResult onKey(FocusNode node, RawKeyEvent event) => - KeyEventResult.ignored; - KeyEventResult onKeyEvent(FocusNode node, KeyEvent event) => KeyEventResult.ignored; @@ -91,7 +87,6 @@ void main() { HookBuilder(builder: (_) { focusNode = useFocusNode( debugLabel: 'Foo', - onKey: onKey, onKeyEvent: onKeyEvent, skipTraversal: true, canRequestFocus: false, @@ -102,7 +97,6 @@ void main() { ); expect(focusNode.debugLabel, 'Foo'); - expect(focusNode.onKey, onKey); expect(focusNode.onKeyEvent, onKeyEvent); expect(focusNode.skipTraversal, true); expect(focusNode.canRequestFocus, false); @@ -110,11 +104,6 @@ void main() { }); testWidgets('handles parameter change', (tester) async { - KeyEventResult onKey(FocusNode node, RawKeyEvent event) => - KeyEventResult.ignored; - KeyEventResult onKey2(FocusNode node, RawKeyEvent event) => - KeyEventResult.ignored; - KeyEventResult onKeyEvent(FocusNode node, KeyEvent event) => KeyEventResult.ignored; KeyEventResult onKeyEvent2(FocusNode node, KeyEvent event) => @@ -125,7 +114,6 @@ void main() { HookBuilder(builder: (_) { focusNode = useFocusNode( debugLabel: 'Foo', - onKey: onKey, onKeyEvent: onKeyEvent, skipTraversal: true, canRequestFocus: false, @@ -140,7 +128,6 @@ void main() { HookBuilder(builder: (_) { focusNode = useFocusNode( debugLabel: 'Bar', - onKey: onKey2, onKeyEvent: onKeyEvent2, ); @@ -148,7 +135,6 @@ void main() { }), ); - expect(focusNode.onKey, onKey2); expect(focusNode.onKeyEvent, onKeyEvent2); expect(focusNode.debugLabel, 'Bar'); expect(focusNode.skipTraversal, false); diff --git a/packages/flutter_hooks/test/use_focus_scope_node_test.dart b/packages/flutter_hooks/test/use_focus_scope_node_test.dart index 587b2350..fcadb77a 100644 --- a/packages/flutter_hooks/test/use_focus_scope_node_test.dart +++ b/packages/flutter_hooks/test/use_focus_scope_node_test.dart @@ -73,15 +73,11 @@ void main() { ); expect(focusScopeNode.debugLabel, official.debugLabel); - expect(focusScopeNode.onKey, official.onKey); expect(focusScopeNode.skipTraversal, official.skipTraversal); expect(focusScopeNode.canRequestFocus, official.canRequestFocus); }); testWidgets('has all the FocusScopeNode parameters', (tester) async { - KeyEventResult onKey(FocusNode node, RawKeyEvent event) => - KeyEventResult.ignored; - KeyEventResult onKeyEvent(FocusNode node, KeyEvent event) => KeyEventResult.ignored; @@ -90,7 +86,6 @@ void main() { HookBuilder(builder: (_) { focusScopeNode = useFocusScopeNode( debugLabel: 'Foo', - onKey: onKey, onKeyEvent: onKeyEvent, skipTraversal: true, canRequestFocus: false, @@ -100,18 +95,12 @@ void main() { ); expect(focusScopeNode.debugLabel, 'Foo'); - expect(focusScopeNode.onKey, onKey); expect(focusScopeNode.onKeyEvent, onKeyEvent); expect(focusScopeNode.skipTraversal, true); expect(focusScopeNode.canRequestFocus, false); }); testWidgets('handles parameter change', (tester) async { - KeyEventResult onKey(FocusNode node, RawKeyEvent event) => - KeyEventResult.ignored; - KeyEventResult onKey2(FocusNode node, RawKeyEvent event) => - KeyEventResult.ignored; - KeyEventResult onKeyEvent(FocusNode node, KeyEvent event) => KeyEventResult.ignored; KeyEventResult onKeyEvent2(FocusNode node, KeyEvent event) => @@ -122,7 +111,6 @@ void main() { HookBuilder(builder: (_) { focusScopeNode = useFocusScopeNode( debugLabel: 'Foo', - onKey: onKey, onKeyEvent: onKeyEvent, skipTraversal: true, canRequestFocus: false, @@ -136,7 +124,6 @@ void main() { HookBuilder(builder: (_) { focusScopeNode = useFocusScopeNode( debugLabel: 'Bar', - onKey: onKey2, onKeyEvent: onKeyEvent2, ); @@ -144,7 +131,6 @@ void main() { }), ); - expect(focusScopeNode.onKey, onKey2); expect(focusScopeNode.onKeyEvent, onKeyEvent2); expect(focusScopeNode.debugLabel, 'Bar'); expect(focusScopeNode.skipTraversal, false); diff --git a/packages/flutter_hooks/test/use_material_states_controller_test.dart b/packages/flutter_hooks/test/use_material_states_controller_test.dart index d30e6275..8592ef90 100644 --- a/packages/flutter_hooks/test/use_material_states_controller_test.dart +++ b/packages/flutter_hooks/test/use_material_states_controller_test.dart @@ -10,7 +10,7 @@ void main() { testWidgets('debugFillProperties', (tester) async { await tester.pumpWidget( HookBuilder(builder: (context) { - useMaterialStatesController(); + useWidgetStatesController(); return const SizedBox(); }), ); @@ -23,44 +23,44 @@ void main() { .toStringDeep(), equalsIgnoringHashCodes( 'HookBuilder\n' - ' │ useMaterialStatesController: MaterialStatesController#00000({})\n' + ' │ useWidgetStatesController: WidgetStatesController#00000({})\n' ' └SizedBox(renderObject: RenderConstrainedBox#00000)\n', ), ); }); - group('useMaterialStatesController', () { + group('useWidgetStatesController', () { testWidgets('initial values matches with real constructor', (tester) async { - late MaterialStatesController controller; - late MaterialStatesController controller2; + late WidgetStatesController controller; + late WidgetStatesController controller2; await tester.pumpWidget( HookBuilder(builder: (context) { - controller2 = MaterialStatesController(); - controller = useMaterialStatesController(); + controller2 = WidgetStatesController(); + controller = useWidgetStatesController(); return Container(); }), ); expect(controller.value, controller2.value); }); - testWidgets("returns a MaterialStatesController that doesn't change", + testWidgets("returns a WidgetStatesController that doesn't change", (tester) async { - late MaterialStatesController controller; - late MaterialStatesController controller2; + late WidgetStatesController controller; + late WidgetStatesController controller2; await tester.pumpWidget( HookBuilder(builder: (context) { - controller = useMaterialStatesController(); + controller = useWidgetStatesController(); return Container(); }), ); - expect(controller, isA()); + expect(controller, isA()); await tester.pumpWidget( HookBuilder(builder: (context) { - controller2 = useMaterialStatesController(); + controller2 = useWidgetStatesController(); return Container(); }), ); @@ -68,15 +68,15 @@ void main() { expect(identical(controller, controller2), isTrue); }); - testWidgets('passes hook parameters to the MaterialStatesController', + testWidgets('passes hook parameters to the WidgetStatesController', (tester) async { - late MaterialStatesController controller; + late WidgetStatesController controller; await tester.pumpWidget( HookBuilder( builder: (context) { - controller = useMaterialStatesController( - values: {MaterialState.selected}, + controller = useWidgetStatesController( + values: {WidgetState.selected}, ); return Container(); @@ -84,17 +84,17 @@ void main() { ), ); - expect(controller.value, {MaterialState.selected}); + expect(controller.value, {WidgetState.selected}); }); - testWidgets('disposes the MaterialStatesController on unmount', + testWidgets('disposes the WidgetStatesController on unmount', (tester) async { - late MaterialStatesController controller; + late WidgetStatesController controller; await tester.pumpWidget( HookBuilder( builder: (context) { - controller = useMaterialStatesController(); + controller = useWidgetStatesController(); return Container(); }, ), From 6e5e6882e911dae85bd78ce255df431d2278b5e5 Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Fri, 22 Mar 2024 12:00:14 +0100 Subject: [PATCH 323/384] flutter_hooks : 0.20.5 -> 0.21.0 --- packages/flutter_hooks/CHANGELOG.md | 2 +- packages/flutter_hooks/pubspec.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/flutter_hooks/CHANGELOG.md b/packages/flutter_hooks/CHANGELOG.md index 571233f3..cbaaaf67 100644 --- a/packages/flutter_hooks/CHANGELOG.md +++ b/packages/flutter_hooks/CHANGELOG.md @@ -1,4 +1,4 @@ -## Unreleased minor +## 0.21.0 - 2024-03-22 - Renamed `useMaterialStatesController` to `useWidgetStatesController` to follow the rename in Flutter. diff --git a/packages/flutter_hooks/pubspec.yaml b/packages/flutter_hooks/pubspec.yaml index f5faba97..bdd3736e 100644 --- a/packages/flutter_hooks/pubspec.yaml +++ b/packages/flutter_hooks/pubspec.yaml @@ -3,7 +3,7 @@ description: A flutter implementation of React hooks. It adds a new kind of widg homepage: https://github.com/rrousselGit/flutter_hooks repository: https://github.com/rrousselGit/flutter_hooks/tree/master/packages/flutter_hooks issue_tracker: https://github.com/rrousselGit/flutter_hooks/issues -version: 0.20.5 +version: 0.21.0 environment: sdk: ">=2.17.0 <3.0.0" From e0a42440c053e26baa365de0edfc203c7c3127d4 Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Mon, 25 Mar 2024 15:42:16 +0100 Subject: [PATCH 324/384] 0.21.1-pre.0 --- packages/flutter_hooks/CHANGELOG.md | 6 +++++- packages/flutter_hooks/pubspec.yaml | 4 ++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/flutter_hooks/CHANGELOG.md b/packages/flutter_hooks/CHANGELOG.md index cbaaaf67..149d4821 100644 --- a/packages/flutter_hooks/CHANGELOG.md +++ b/packages/flutter_hooks/CHANGELOG.md @@ -1,4 +1,8 @@ -## 0.21.0 - 2024-03-22 +## 0.21.1-pre.0 + +- Bump minimum Flutter SDK to 3.21.0-13.0.pre.4 + +## 0.21.0 - 2024-03-22 (retracted) - Renamed `useMaterialStatesController` to `useWidgetStatesController` to follow the rename in Flutter. diff --git a/packages/flutter_hooks/pubspec.yaml b/packages/flutter_hooks/pubspec.yaml index bdd3736e..22adac87 100644 --- a/packages/flutter_hooks/pubspec.yaml +++ b/packages/flutter_hooks/pubspec.yaml @@ -3,11 +3,11 @@ description: A flutter implementation of React hooks. It adds a new kind of widg homepage: https://github.com/rrousselGit/flutter_hooks repository: https://github.com/rrousselGit/flutter_hooks/tree/master/packages/flutter_hooks issue_tracker: https://github.com/rrousselGit/flutter_hooks/issues -version: 0.21.0 +version: 0.21.1-pre.0 environment: sdk: ">=2.17.0 <3.0.0" - flutter: ">=3.19.0-0.3.pre" + flutter: ">=3.21.0-13.0.pre.4" dependencies: flutter: From 1e8a94704221485ef5c413521a4585be07b545cb Mon Sep 17 00:00:00 2001 From: Nate Date: Thu, 30 May 2024 04:02:30 -0600 Subject: [PATCH 325/384] modernize README code snippets (#429) --- README.md | 14 ++++++-------- .../resources/translations/ko_kr/README.md | 14 ++++++-------- .../resources/translations/pt_br/README.md | 12 ++++-------- .../resources/translations/zh_cn/README.md | 11 +++++------ 4 files changed, 21 insertions(+), 30 deletions(-) diff --git a/README.md b/README.md index 424d118d..bd06b176 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 with SingleTickerProviderStateMixin { - AnimationController? _controller; + late final AnimationController _controller; @override void initState() { @@ -41,13 +40,13 @@ class _ExampleState extends State 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; diff --git a/packages/flutter_hooks/resources/translations/ko_kr/README.md b/packages/flutter_hooks/resources/translations/ko_kr/README.md index 2ae0f869..53428f3b 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 with SingleTickerProviderStateMixin { - AnimationController? _controller; + late final AnimationController _controller; @override void initState() { @@ -39,13 +38,13 @@ class _ExampleState extends State 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 60b787c6..761fdf9b 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 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 ebd3caf3..e33f13d0 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 with SingleTickerProviderStateMixin { - AnimationController? _controller; + late final AnimationController _controller; @override void initState() { @@ -42,13 +41,13 @@ class _ExampleState extends State 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(); } From 4ade5c13d2ceba1f99b08de4a4ba67af2a45a051 Mon Sep 17 00:00:00 2001 From: Moshe Dicker <75931499+dickermoshe@users.noreply.github.com> Date: Mon, 8 Jul 2024 01:44:26 -0400 Subject: [PATCH 326/384] add draggable_scrollable_controller (#417) --- README.md | 51 +++---- packages/flutter_hooks/CHANGELOG.md | 4 + .../src/draggable_scrollable_controller.dart | 34 +++++ packages/flutter_hooks/lib/src/hooks.dart | 2 + ..._draggable_scrollable_controller_test.dart | 134 ++++++++++++++++++ 5 files changed, 200 insertions(+), 25 deletions(-) create mode 100644 packages/flutter_hooks/lib/src/draggable_scrollable_controller.dart create mode 100644 packages/flutter_hooks/test/use_draggable_scrollable_controller_test.dart diff --git a/README.md b/README.md index bd06b176..f17108b3 100644 --- a/README.md +++ b/README.md @@ -308,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: @@ -336,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 149d4821..47efce35 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 00000000..621a3392 --- /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? keys, +}) { + return use(_DraggableScrollableControllerHook(keys: keys)); +} + +class _DraggableScrollableControllerHook + extends Hook { + const _DraggableScrollableControllerHook({super.keys}); + + @override + HookState> + 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 859df563..2eef50c5 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/test/use_draggable_scrollable_controller_test.dart b/packages/flutter_hooks/test/use_draggable_scrollable_controller_test.dart new file mode 100644 index 00000000..57729b8e --- /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); + }); + }); +} From 346f47c97b5856348c97de056658298b8dd58564 Mon Sep 17 00:00:00 2001 From: Tatsuya Hirano <44150538+dev-tatsuya@users.noreply.github.com> Date: Mon, 8 Jul 2024 15:08:33 +0900 Subject: [PATCH 327/384] Update useSingleTickerProvider based on SingleTickerProviderStateMixin (#432) Co-authored-by: Remi Rousselet --- packages/flutter_hooks/lib/src/animation.dart | 28 +++++++++++++++++-- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/packages/flutter_hooks/lib/src/animation.dart b/packages/flutter_hooks/lib/src/animation.dart index bce00cbe..86c0c8ce 100644 --- a/packages/flutter_hooks/lib/src/animation.dart +++ b/packages/flutter_hooks/lib/src/animation.dart @@ -171,6 +171,7 @@ class _TickerProviderHookState extends HookState implements TickerProvider { Ticker? _ticker; + ValueListenable? _tickerModeNotifier; @override Ticker createTicker(TickerCallback onTick) { @@ -184,7 +185,10 @@ class _TickerProviderHookState 'If you need multiple Ticker, consider using useSingleTickerProvider multiple times ' 'to create as many Tickers as needed.'); }(), ''); - return _ticker = Ticker(onTick, debugLabel: 'created by $context'); + _ticker = Ticker(onTick, debugLabel: 'created by $context'); + _updateTickerModeNotifier(); + _updateTicker(); + return _ticker!; } @override @@ -199,14 +203,32 @@ class _TickerProviderHookState ' by AnimationControllers should be disposed by calling dispose() on ' ' the AnimationController itself. Otherwise, the ticker will leak.\n'); }(), ''); + _tickerModeNotifier?.removeListener(_updateTicker); + _tickerModeNotifier = null; + super.dispose(); } @override TickerProvider build(BuildContext context) { + _updateTickerModeNotifier(); + _updateTicker(); + return this; + } + + void _updateTicker() { if (_ticker != null) { - _ticker!.muted = !TickerMode.of(context); + _ticker!.muted = !_tickerModeNotifier!.value; } - return this; + } + + void _updateTickerModeNotifier() { + final newNotifier = TickerMode.getNotifier(context); + if (newNotifier == _tickerModeNotifier) { + return; + } + _tickerModeNotifier?.removeListener(_updateTicker); + newNotifier.addListener(_updateTicker); + _tickerModeNotifier = newNotifier; } @override From 8052a7093e6e2a4f14789ef32ddbb9fc8245dab5 Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Mon, 8 Jul 2024 08:14:26 +0200 Subject: [PATCH 328/384] Format --- ..._draggable_scrollable_controller_test.dart | 78 ++++++++++--------- 1 file changed, 42 insertions(+), 36 deletions(-) diff --git a/packages/flutter_hooks/test/use_draggable_scrollable_controller_test.dart b/packages/flutter_hooks/test/use_draggable_scrollable_controller_test.dart index 57729b8e..9c2e32d5 100644 --- a/packages/flutter_hooks/test/use_draggable_scrollable_controller_test.dart +++ b/packages/flutter_hooks/test/use_draggable_scrollable_controller_test.dart @@ -42,54 +42,57 @@ void main() { 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) { + 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: controller2, + controller: controller, builder: (context, scrollController) { return ListView.builder( controller: scrollController, itemCount: 100, itemBuilder: (context, index) { return ListTile( - title: Text('Item $index on Sheet 2'), + title: Text('Item $index on Sheet 1'), ); }, ); }, ); }); - }, - child: Text("Open Sheet 2")) + }); + }, + child: const 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: const Text('Open Sheet 2'), + ) ], ); }), @@ -115,7 +118,10 @@ void main() { final controller2IsAttached = controller2.isAttached; // Close Sheet 2 by dragging it down await tester.fling( - find.byType(DraggableScrollableSheet), const Offset(0, 500), 300); + find.byType(DraggableScrollableSheet), + const Offset(0, 500), + 300, + ); await tester.pumpAndSettle(); // Compare the initial values of the two controllers From 9731dd2fdccb6d56606167578daccf840ffd9fdc Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Mon, 8 Jul 2024 08:16:37 +0200 Subject: [PATCH 329/384] flutter_hooks : 0.21.1-pre.0 -> 0.21.1 --- packages/flutter_hooks/CHANGELOG.md | 3 ++- packages/flutter_hooks/pubspec.yaml | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/flutter_hooks/CHANGELOG.md b/packages/flutter_hooks/CHANGELOG.md index 47efce35..70640b75 100644 --- a/packages/flutter_hooks/CHANGELOG.md +++ b/packages/flutter_hooks/CHANGELOG.md @@ -1,6 +1,7 @@ -## Unreleased patch +## 0.21.1-pre.1 - 2024-07-08 - Added `useDraggableScrollableController` (@thanks to @dickermoshe) +- Fix TickerMode not getting muted by hooks (thanks to @dev-tatsuya) ## 0.21.1-pre.0 diff --git a/packages/flutter_hooks/pubspec.yaml b/packages/flutter_hooks/pubspec.yaml index 22adac87..84d2f090 100644 --- a/packages/flutter_hooks/pubspec.yaml +++ b/packages/flutter_hooks/pubspec.yaml @@ -3,7 +3,7 @@ description: A flutter implementation of React hooks. It adds a new kind of widg homepage: https://github.com/rrousselGit/flutter_hooks repository: https://github.com/rrousselGit/flutter_hooks/tree/master/packages/flutter_hooks issue_tracker: https://github.com/rrousselGit/flutter_hooks/issues -version: 0.21.1-pre.0 +version: 0.21.1-pre.1 environment: sdk: ">=2.17.0 <3.0.0" From 233466fc9b0368432ad9a6e3246a410daf4975d6 Mon Sep 17 00:00:00 2001 From: Tim Lehmann Date: Mon, 22 Jul 2024 12:43:46 +0200 Subject: [PATCH 330/384] Add `onAttach` and `onDetach` to `useScrollController` and `usePageController` (#436) --- packages/flutter_hooks/CHANGELOG.md | 6 ++++- .../lib/src/page_controller.dart | 10 ++++++++ .../lib/src/scroll_controller.dart | 10 ++++++++ .../test/use_page_controller_test.dart | 24 +++++++++++++++++++ .../test/use_scroll_controller_test.dart | 23 ++++++++++++++++++ 5 files changed, 72 insertions(+), 1 deletion(-) diff --git a/packages/flutter_hooks/CHANGELOG.md b/packages/flutter_hooks/CHANGELOG.md index 70640b75..dce500e8 100644 --- a/packages/flutter_hooks/CHANGELOG.md +++ b/packages/flutter_hooks/CHANGELOG.md @@ -1,6 +1,10 @@ +## Unreleased patch + +- Added `onAttach` and `onDetach` to `useScrollController` and `usePageController` (thanks to @whynotmake-it) + ## 0.21.1-pre.1 - 2024-07-08 -- Added `useDraggableScrollableController` (@thanks to @dickermoshe) +- Added `useDraggableScrollableController` (thanks to @dickermoshe) - Fix TickerMode not getting muted by hooks (thanks to @dev-tatsuya) ## 0.21.1-pre.0 diff --git a/packages/flutter_hooks/lib/src/page_controller.dart b/packages/flutter_hooks/lib/src/page_controller.dart index ea0d7945..bbe16734 100644 --- a/packages/flutter_hooks/lib/src/page_controller.dart +++ b/packages/flutter_hooks/lib/src/page_controller.dart @@ -8,6 +8,8 @@ PageController usePageController({ int initialPage = 0, bool keepPage = true, double viewportFraction = 1.0, + ScrollControllerCallback? onAttach, + ScrollControllerCallback? onDetach, List? keys, }) { return use( @@ -15,6 +17,8 @@ PageController usePageController({ initialPage: initialPage, keepPage: keepPage, viewportFraction: viewportFraction, + onAttach: onAttach, + onDetach: onDetach, keys: keys, ), ); @@ -25,12 +29,16 @@ class _PageControllerHook extends Hook { required this.initialPage, required this.keepPage, required this.viewportFraction, + this.onAttach, + this.onDetach, List? keys, }) : super(keys: keys); final int initialPage; final bool keepPage; final double viewportFraction; + final ScrollControllerCallback? onAttach; + final ScrollControllerCallback? onDetach; @override HookState> createState() => @@ -43,6 +51,8 @@ class _PageControllerHookState initialPage: hook.initialPage, keepPage: hook.keepPage, viewportFraction: hook.viewportFraction, + onAttach: hook.onAttach, + onDetach: hook.onDetach, ); @override diff --git a/packages/flutter_hooks/lib/src/scroll_controller.dart b/packages/flutter_hooks/lib/src/scroll_controller.dart index 7a6a539f..1d182cf6 100644 --- a/packages/flutter_hooks/lib/src/scroll_controller.dart +++ b/packages/flutter_hooks/lib/src/scroll_controller.dart @@ -8,6 +8,8 @@ ScrollController useScrollController({ double initialScrollOffset = 0.0, bool keepScrollOffset = true, String? debugLabel, + ScrollControllerCallback? onAttach, + ScrollControllerCallback? onDetach, List? keys, }) { return use( @@ -15,6 +17,8 @@ ScrollController useScrollController({ initialScrollOffset: initialScrollOffset, keepScrollOffset: keepScrollOffset, debugLabel: debugLabel, + onAttach: onAttach, + onDetach: onDetach, keys: keys, ), ); @@ -25,12 +29,16 @@ class _ScrollControllerHook extends Hook { required this.initialScrollOffset, required this.keepScrollOffset, this.debugLabel, + this.onAttach, + this.onDetach, List? keys, }) : super(keys: keys); final double initialScrollOffset; final bool keepScrollOffset; final String? debugLabel; + final ScrollControllerCallback? onAttach; + final ScrollControllerCallback? onDetach; @override HookState> createState() => @@ -43,6 +51,8 @@ class _ScrollControllerHookState initialScrollOffset: hook.initialScrollOffset, keepScrollOffset: hook.keepScrollOffset, debugLabel: hook.debugLabel, + onAttach: hook.onAttach, + onDetach: hook.onDetach, ); @override diff --git a/packages/flutter_hooks/test/use_page_controller_test.dart b/packages/flutter_hooks/test/use_page_controller_test.dart index da4def3a..31f0cf99 100644 --- a/packages/flutter_hooks/test/use_page_controller_test.dart +++ b/packages/flutter_hooks/test/use_page_controller_test.dart @@ -72,6 +72,9 @@ void main() { testWidgets('passes hook parameters to the PageController', (tester) async { late PageController controller; + void onAttach(ScrollPosition position) {} + void onDetach(ScrollPosition position) {} + await tester.pumpWidget( HookBuilder( builder: (context) { @@ -79,6 +82,8 @@ void main() { initialPage: 42, keepPage: false, viewportFraction: 3.4, + onAttach: onAttach, + onDetach: onDetach, ); return Container(); @@ -89,6 +94,25 @@ void main() { expect(controller.initialPage, 42); expect(controller.keepPage, false); expect(controller.viewportFraction, 3.4); + expect(controller.onAttach, onAttach); + 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 { diff --git a/packages/flutter_hooks/test/use_scroll_controller_test.dart b/packages/flutter_hooks/test/use_scroll_controller_test.dart index 7ae60b47..3ab90b21 100644 --- a/packages/flutter_hooks/test/use_scroll_controller_test.dart +++ b/packages/flutter_hooks/test/use_scroll_controller_test.dart @@ -73,6 +73,9 @@ void main() { (tester) async { late ScrollController controller; + void onAttach(ScrollPosition position) {} + void onDetach(ScrollPosition position) {} + await tester.pumpWidget( HookBuilder( builder: (context) { @@ -80,6 +83,8 @@ void main() { initialScrollOffset: 42, debugLabel: 'Hello', keepScrollOffset: false, + onAttach: onAttach, + onDetach: onDetach, ); return Container(); @@ -90,6 +95,24 @@ void main() { expect(controller.initialScrollOffset, 42); expect(controller.debugLabel, 'Hello'); expect(controller.keepScrollOffset, false); + 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); }); }); } From bcafa1501e21a3df6c835b400888f9c15b3e790f Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Mon, 22 Jul 2024 12:44:26 +0200 Subject: [PATCH 331/384] flutter_hooks : 0.21.1-pre.1 -> 0.21.1-pre.2 --- packages/flutter_hooks/CHANGELOG.md | 2 +- packages/flutter_hooks/pubspec.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/flutter_hooks/CHANGELOG.md b/packages/flutter_hooks/CHANGELOG.md index dce500e8..36b629a7 100644 --- a/packages/flutter_hooks/CHANGELOG.md +++ b/packages/flutter_hooks/CHANGELOG.md @@ -1,4 +1,4 @@ -## Unreleased patch +## 0.21.1-pre.2 - 2024-07-22 - Added `onAttach` and `onDetach` to `useScrollController` and `usePageController` (thanks to @whynotmake-it) diff --git a/packages/flutter_hooks/pubspec.yaml b/packages/flutter_hooks/pubspec.yaml index 84d2f090..820fa65d 100644 --- a/packages/flutter_hooks/pubspec.yaml +++ b/packages/flutter_hooks/pubspec.yaml @@ -3,7 +3,7 @@ description: A flutter implementation of React hooks. It adds a new kind of widg homepage: https://github.com/rrousselGit/flutter_hooks repository: https://github.com/rrousselGit/flutter_hooks/tree/master/packages/flutter_hooks issue_tracker: https://github.com/rrousselGit/flutter_hooks/issues -version: 0.21.1-pre.1 +version: 0.21.1-pre.2 environment: sdk: ">=2.17.0 <3.0.0" From 144f38e6ff16a8bb86639deb4920798c0d38c41b Mon Sep 17 00:00:00 2001 From: Tim Lehmann Date: Tue, 30 Jul 2024 15:21:46 +0200 Subject: [PATCH 332/384] Add `useOnListenableChange` (#438) --- README.md | 1 + packages/flutter_hooks/CHANGELOG.md | 3 + .../flutter_hooks/lib/src/listenable.dart | 68 ++++++++ .../test/use_on_listenable_change_test.dart | 165 ++++++++++++++++++ 4 files changed, 237 insertions(+) create mode 100644 packages/flutter_hooks/test/use_on_listenable_change_test.dart diff --git a/README.md b/README.md index f17108b3..984f33b3 100644 --- a/README.md +++ b/README.md @@ -331,6 +331,7 @@ They will take care of creating/updating/disposing an object. | [useListenableSelector](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useListenableSelector.html) | Similar to `useListenable`, but allows filtering UI rebuilds | | [useValueNotifier](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useValueNotifier.html) | Creates a `ValueNotifier` which will be automatically disposed. | | [useValueListenable](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useValueListenable.html) | Subscribes to a `ValueListenable` and return its value. | +| [useOnListenableChange](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useOnListenableChange.html) | Adds a given listener callback to a `Listenable` which will be automatically removed. | #### Misc hooks: diff --git a/packages/flutter_hooks/CHANGELOG.md b/packages/flutter_hooks/CHANGELOG.md index 36b629a7..1727c2a8 100644 --- a/packages/flutter_hooks/CHANGELOG.md +++ b/packages/flutter_hooks/CHANGELOG.md @@ -1,3 +1,6 @@ +## Unreleased patch +- Added `useOnListenableChange` (thanks to @whynotmake-it) + ## 0.21.1-pre.2 - 2024-07-22 - Added `onAttach` and `onDetach` to `useScrollController` and `usePageController` (thanks to @whynotmake-it) diff --git a/packages/flutter_hooks/lib/src/listenable.dart b/packages/flutter_hooks/lib/src/listenable.dart index 1284f2dc..39ce2edf 100644 --- a/packages/flutter_hooks/lib/src/listenable.dart +++ b/packages/flutter_hooks/lib/src/listenable.dart @@ -128,3 +128,71 @@ class _UseValueNotifierHookState @override String get debugLabel => 'useValueNotifier'; } + +/// Adds a given [listener] to a [Listenable] and removes it when the hook is +/// disposed. +/// +/// As opposed to `useListenable`, this hook does not mark the widget as needing +/// build when the listener is called. Use this for side effects that do not +/// require a rebuild. +/// +/// See also: +/// * [Listenable] +/// * [ValueListenable] +/// * [useListenable] +void useOnListenableChange( + Listenable? listenable, + VoidCallback listener, +) { + return use(_OnListenableChangeHook(listenable, listener)); +} + +class _OnListenableChangeHook extends Hook { + const _OnListenableChangeHook( + this.listenable, + this.listener, + ); + + final Listenable? listenable; + final VoidCallback listener; + + @override + _OnListenableChangeHookState createState() => _OnListenableChangeHookState(); +} + +class _OnListenableChangeHookState + extends HookState { + @override + void initHook() { + super.initHook(); + hook.listenable?.addListener(_listener); + } + + @override + void didUpdateHook(_OnListenableChangeHook oldHook) { + super.didUpdateHook(oldHook); + if (hook.listenable != oldHook.listenable) { + oldHook.listenable?.removeListener(_listener); + hook.listenable?.addListener(_listener); + } + } + + @override + void build(BuildContext context) {} + + @override + void dispose() { + hook.listenable?.removeListener(_listener); + } + + /// Wraps `hook.listener` so we have a non-changing reference to it. + void _listener() { + hook.listener(); + } + + @override + String get debugLabel => 'useOnListenableChange'; + + @override + Object? get debugValue => hook.listenable; +} diff --git a/packages/flutter_hooks/test/use_on_listenable_change_test.dart b/packages/flutter_hooks/test/use_on_listenable_change_test.dart new file mode 100644 index 00000000..35cc10e5 --- /dev/null +++ b/packages/flutter_hooks/test/use_on_listenable_change_test.dart @@ -0,0 +1,165 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; + +import 'mock.dart'; + +void main() { + testWidgets('debugFillProperties', (tester) async { + final listenable = ValueNotifier(42); + + await tester.pumpWidget( + HookBuilder(builder: (context) { + useOnListenableChange(listenable, () {}); + return const SizedBox(); + }), + ); + + await tester.pump(); + + final element = tester.element(find.byType(HookBuilder)); + + expect( + element + .toDiagnosticsNode(style: DiagnosticsTreeStyle.offstage) + .toStringDeep(), + equalsIgnoringHashCodes( + 'HookBuilder\n' + ' │ useOnListenableChange: ValueNotifier#00000(42)\n' + ' └SizedBox(renderObject: RenderConstrainedBox#00000)\n', + ), + ); + }); + + testWidgets('calls listener when Listenable updates', (tester) async { + final listenable = ValueNotifier(42); + + int? value; + + await tester.pumpWidget( + HookBuilder(builder: (context) { + useOnListenableChange( + listenable, + () => value = listenable.value, + ); + return const SizedBox(); + }), + ); + + expect(value, isNull); + listenable.value++; + expect(value, 43); + }); + + testWidgets( + 'listens new Listenable when Listenable is changed', + (tester) async { + final listenable1 = ValueNotifier(42); + final listenable2 = ValueNotifier(42); + + await tester.pumpWidget( + HookBuilder( + builder: (context) { + useOnListenableChange(listenable1, () {}); + return const SizedBox(); + }, + ), + ); + + await tester.pumpWidget( + HookBuilder( + builder: (context) { + useOnListenableChange(listenable2, () {}); + return const SizedBox(); + }, + ), + ); + + // ignore: invalid_use_of_protected_member + expect(listenable1.hasListeners, isFalse); + // ignore: invalid_use_of_protected_member + expect(listenable2.hasListeners, isTrue); + }, + ); + + testWidgets( + 'listens new listener when listener is changed', + (tester) async { + final listenable = ValueNotifier(42); + late final int value; + + void listener1() { + throw StateError('listener1 should not have been called'); + } + + void listener2() { + value = listenable.value; + } + + await tester.pumpWidget( + HookBuilder( + builder: (context) { + useOnListenableChange(listenable, listener1); + return const SizedBox(); + }, + ), + ); + + await tester.pumpWidget( + HookBuilder( + builder: (context) { + useOnListenableChange(listenable, listener2); + return const SizedBox(); + }, + ), + ); + + listenable.value++; + // By now, we should have subscribed to listener2, which sets the value + expect(value, 43); + }, + ); + + testWidgets('unsubscribes when listenable becomes null', (tester) async { + final listenable = ValueNotifier(42); + + await tester.pumpWidget( + HookBuilder(builder: (context) { + useOnListenableChange(listenable, () {}); + return const SizedBox(); + }), + ); + + // ignore: invalid_use_of_protected_member + expect(listenable.hasListeners, isTrue); + + await tester.pumpWidget( + HookBuilder(builder: (context) { + useOnListenableChange(null, () {}); + return const SizedBox(); + }), + ); + + // ignore: invalid_use_of_protected_member + expect(listenable.hasListeners, isFalse); + }); + + testWidgets('unsubscribes when disposed', (tester) async { + final listenable = ValueNotifier(42); + + await tester.pumpWidget( + HookBuilder(builder: (context) { + useOnListenableChange(listenable, () {}); + return const SizedBox(); + }), + ); + + // ignore: invalid_use_of_protected_member + expect(listenable.hasListeners, isTrue); + + await tester.pumpWidget(Container()); + + // ignore: invalid_use_of_protected_member + expect(listenable.hasListeners, isFalse); + }); +} From a24abe058adf8478986fd6137dbc3764fb8b2912 Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Tue, 30 Jul 2024 15:22:27 +0200 Subject: [PATCH 333/384] flutter_hooks : 0.21.1-pre.2 -> 0.21.1-pre.3 --- packages/flutter_hooks/CHANGELOG.md | 2 +- packages/flutter_hooks/pubspec.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/flutter_hooks/CHANGELOG.md b/packages/flutter_hooks/CHANGELOG.md index 1727c2a8..90c4b443 100644 --- a/packages/flutter_hooks/CHANGELOG.md +++ b/packages/flutter_hooks/CHANGELOG.md @@ -1,4 +1,4 @@ -## Unreleased patch +## 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/pubspec.yaml b/packages/flutter_hooks/pubspec.yaml index 820fa65d..430af953 100644 --- a/packages/flutter_hooks/pubspec.yaml +++ b/packages/flutter_hooks/pubspec.yaml @@ -3,7 +3,7 @@ description: A flutter implementation of React hooks. It adds a new kind of widg homepage: https://github.com/rrousselGit/flutter_hooks repository: https://github.com/rrousselGit/flutter_hooks/tree/master/packages/flutter_hooks issue_tracker: https://github.com/rrousselGit/flutter_hooks/issues -version: 0.21.1-pre.2 +version: 0.21.1-pre.3 environment: sdk: ">=2.17.0 <3.0.0" From 61d96fc65592802bf8fe145f43047c4876175519 Mon Sep 17 00:00:00 2001 From: Tim Lehmann Date: Thu, 1 Aug 2024 16:14:16 +0200 Subject: [PATCH 334/384] Add `useFixedExtentScrollController` (#437) --- README.md | 1 + packages/flutter_hooks/CHANGELOG.md | 5 + .../src/fixed_extent_scroll_controller.dart | 57 ++++++++++ packages/flutter_hooks/lib/src/hooks.dart | 1 + ...e_fixed_extent_scroll_controller_test.dart | 102 ++++++++++++++++++ .../test/use_page_controller_test.dart | 19 +--- .../test/use_scroll_controller_test.dart | 18 +--- 7 files changed, 170 insertions(+), 33 deletions(-) create mode 100644 packages/flutter_hooks/lib/src/fixed_extent_scroll_controller.dart create mode 100644 packages/flutter_hooks/test/use_fixed_extent_scroll_controller_test.dart diff --git a/README.md b/README.md index 984f33b3..99aff235 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 90c4b443..68355249 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 00000000..a70a1619 --- /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? keys, +}) { + return use( + _FixedExtentScrollControllerHook( + initialItem: initialItem, + onAttach: onAttach, + onDetach: onDetach, + keys: keys, + ), + ); +} + +class _FixedExtentScrollControllerHook + extends Hook { + const _FixedExtentScrollControllerHook({ + required this.initialItem, + this.onAttach, + this.onDetach, + super.keys, + }); + + final int initialItem; + final ScrollControllerCallback? onAttach; + final ScrollControllerCallback? onDetach; + + @override + HookState> + 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 2eef50c5..023868de 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'; 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 00000000..00f9f6c8 --- /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()); + + 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 31f0cf99..7015090e 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 3ab90b21..fee5fd52 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); - }); }); } From 801ec751e800054ce1cb0a7c4c21b2c23f6756e6 Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Thu, 1 Aug 2024 16:14:49 +0200 Subject: [PATCH 335/384] flutter_hooks : 0.21.1-pre.3 -> 0.21.1-pre.4 --- packages/flutter_hooks/CHANGELOG.md | 2 +- packages/flutter_hooks/pubspec.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/flutter_hooks/CHANGELOG.md b/packages/flutter_hooks/CHANGELOG.md index 68355249..952422b9 100644 --- a/packages/flutter_hooks/CHANGELOG.md +++ b/packages/flutter_hooks/CHANGELOG.md @@ -1,4 +1,4 @@ -## Unreleased build +## 0.21.1-pre.4 - 2024-08-01 - Added `useFixedExtentScrollController` (thanks to @whynotmake-it) diff --git a/packages/flutter_hooks/pubspec.yaml b/packages/flutter_hooks/pubspec.yaml index 430af953..004b7802 100644 --- a/packages/flutter_hooks/pubspec.yaml +++ b/packages/flutter_hooks/pubspec.yaml @@ -3,7 +3,7 @@ description: A flutter implementation of React hooks. It adds a new kind of widg homepage: https://github.com/rrousselGit/flutter_hooks repository: https://github.com/rrousselGit/flutter_hooks/tree/master/packages/flutter_hooks issue_tracker: https://github.com/rrousselGit/flutter_hooks/issues -version: 0.21.1-pre.3 +version: 0.21.1-pre.4 environment: sdk: ">=2.17.0 <3.0.0" From fe371d441705a882c1848b7b1fd2fe7bab428f9b Mon Sep 17 00:00:00 2001 From: d-polikhranidi <76728763+d-polikhranidi@users.noreply.github.com> Date: Fri, 27 Sep 2024 00:06:57 +0300 Subject: [PATCH 336/384] Add animationDuration property to useTabController hook --- .../flutter_hooks/lib/src/tab_controller.dart | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/packages/flutter_hooks/lib/src/tab_controller.dart b/packages/flutter_hooks/lib/src/tab_controller.dart index 5aba96c3..ba40e36f 100644 --- a/packages/flutter_hooks/lib/src/tab_controller.dart +++ b/packages/flutter_hooks/lib/src/tab_controller.dart @@ -6,6 +6,7 @@ part of 'hooks.dart'; /// - [TabController] TabController useTabController({ required int initialLength, + Duration? animationDuration = kTabScrollDuration, TickerProvider? vsync, int initialIndex = 0, List? keys, @@ -17,6 +18,7 @@ TabController useTabController({ vsync: vsync, length: initialLength, initialIndex: initialIndex, + animationDuration: animationDuration, keys: keys, ), ); @@ -27,23 +29,24 @@ class _TabControllerHook extends Hook { required this.length, required this.vsync, required this.initialIndex, - List? keys, - }) : super(keys: keys); + required this.animationDuration, + super.keys, + }); final int length; final TickerProvider vsync; final int initialIndex; + final Duration? animationDuration; @override - HookState> createState() => - _TabControllerHookState(); + HookState> createState() => _TabControllerHookState(); } -class _TabControllerHookState - extends HookState { +class _TabControllerHookState extends HookState { late final controller = TabController( length: hook.length, initialIndex: hook.initialIndex, + animationDuration: hook.animationDuration, vsync: hook.vsync, ); From 38f78f531f7979ce34f7c98ff8780238bca89b33 Mon Sep 17 00:00:00 2001 From: d-polikhranidi <76728763+d-polikhranidi@users.noreply.github.com> Date: Wed, 2 Oct 2024 13:55:25 +0300 Subject: [PATCH 337/384] Update use_tab_controller_test.dart --- .../test/use_tab_controller_test.dart | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/packages/flutter_hooks/test/use_tab_controller_test.dart b/packages/flutter_hooks/test/use_tab_controller_test.dart index 80ddaf97..dd207dc0 100644 --- a/packages/flutter_hooks/test/use_tab_controller_test.dart +++ b/packages/flutter_hooks/test/use_tab_controller_test.dart @@ -138,6 +138,27 @@ void main() { verifyNoMoreInteractions(vsync); ticker.dispose(); }); + testWidgets('initial animationDuration matches with real constructor', (tester) async { + late TabController controller; + late TabController controller2; + + final vsync = TickerProviderMock(); + final ticker = Ticker((_) {}); + when(vsync.createTicker((_) {})).thenReturn(ticker); + + await tester.pumpWidget( + HookBuilder( + builder: (context) { + controller = useTabController(initialLength: 4); + controller2 = TabController(length: 4, vsync: vsync); + return Container(); + }, + ), + ); + + expect(controller.animationDuration, controller2.animationDuration); + + }); }); } From 55c9bbf1a29235e39c9e3099657500bf28e2e87f Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Wed, 2 Oct 2024 12:57:30 +0200 Subject: [PATCH 338/384] Update packages/flutter_hooks/test/use_tab_controller_test.dart --- packages/flutter_hooks/test/use_tab_controller_test.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/flutter_hooks/test/use_tab_controller_test.dart b/packages/flutter_hooks/test/use_tab_controller_test.dart index dd207dc0..3abc85b8 100644 --- a/packages/flutter_hooks/test/use_tab_controller_test.dart +++ b/packages/flutter_hooks/test/use_tab_controller_test.dart @@ -138,6 +138,7 @@ void main() { verifyNoMoreInteractions(vsync); ticker.dispose(); }); + testWidgets('initial animationDuration matches with real constructor', (tester) async { late TabController controller; late TabController controller2; From c6c2a2a48a90494513f07f183639176c1538006c Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Wed, 2 Oct 2024 12:57:34 +0200 Subject: [PATCH 339/384] Update packages/flutter_hooks/test/use_tab_controller_test.dart --- packages/flutter_hooks/test/use_tab_controller_test.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/flutter_hooks/test/use_tab_controller_test.dart b/packages/flutter_hooks/test/use_tab_controller_test.dart index 3abc85b8..c12c5331 100644 --- a/packages/flutter_hooks/test/use_tab_controller_test.dart +++ b/packages/flutter_hooks/test/use_tab_controller_test.dart @@ -158,7 +158,6 @@ void main() { ); expect(controller.animationDuration, controller2.animationDuration); - }); }); } From 2c015bf9c8a273380a0ed17145abb06258328a7c Mon Sep 17 00:00:00 2001 From: d-polikhranidi <76728763+d-polikhranidi@users.noreply.github.com> Date: Wed, 2 Oct 2024 14:03:38 +0300 Subject: [PATCH 340/384] Update use_tab_controller_test.dart --- packages/flutter_hooks/test/use_tab_controller_test.dart | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/packages/flutter_hooks/test/use_tab_controller_test.dart b/packages/flutter_hooks/test/use_tab_controller_test.dart index c12c5331..84e7d57e 100644 --- a/packages/flutter_hooks/test/use_tab_controller_test.dart +++ b/packages/flutter_hooks/test/use_tab_controller_test.dart @@ -143,13 +143,10 @@ void main() { late TabController controller; late TabController controller2; - final vsync = TickerProviderMock(); - final ticker = Ticker((_) {}); - when(vsync.createTicker((_) {})).thenReturn(ticker); - await tester.pumpWidget( HookBuilder( builder: (context) { + final vsync = useSingleTickerProvider(); controller = useTabController(initialLength: 4); controller2 = TabController(length: 4, vsync: vsync); return Container(); From 12e52a68c276b13a50f98ec93e8288d8ba3215d5 Mon Sep 17 00:00:00 2001 From: Dalis Date: Wed, 2 Oct 2024 14:28:48 +0300 Subject: [PATCH 341/384] dart format and update import in hooks.dart --- packages/flutter_hooks/lib/src/hooks.dart | 9 +-------- packages/flutter_hooks/test/use_tab_controller_test.dart | 4 +--- 2 files changed, 2 insertions(+), 11 deletions(-) diff --git a/packages/flutter_hooks/lib/src/hooks.dart b/packages/flutter_hooks/lib/src/hooks.dart index 023868de..1a77c602 100644 --- a/packages/flutter_hooks/lib/src/hooks.dart +++ b/packages/flutter_hooks/lib/src/hooks.dart @@ -2,14 +2,7 @@ import 'dart:async'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart' - show - Brightness, - DraggableScrollableController, - ExpansionTileController, - WidgetStatesController, - SearchController, - TabController, - WidgetState; + show Brightness, DraggableScrollableController, ExpansionTileController, SearchController, TabController, WidgetState, WidgetStatesController, kTabScrollDuration; import 'package:flutter/scheduler.dart'; import 'package:flutter/widgets.dart'; diff --git a/packages/flutter_hooks/test/use_tab_controller_test.dart b/packages/flutter_hooks/test/use_tab_controller_test.dart index 84e7d57e..f93b1641 100644 --- a/packages/flutter_hooks/test/use_tab_controller_test.dart +++ b/packages/flutter_hooks/test/use_tab_controller_test.dart @@ -20,9 +20,7 @@ void main() { final element = tester.element(find.byType(HookBuilder)); expect( - element - .toDiagnosticsNode(style: DiagnosticsTreeStyle.offstage) - .toStringDeep(), + element.toDiagnosticsNode(style: DiagnosticsTreeStyle.offstage).toStringDeep(), equalsIgnoringHashCodes( 'HookBuilder\n' ' │ useSingleTickerProvider\n' From 606fabc17845e81fca62f2451dd74fadd847781f Mon Sep 17 00:00:00 2001 From: ume-kun1015 Date: Wed, 11 Dec 2024 00:16:54 +0900 Subject: [PATCH 342/384] feat: add useOverlayPortalController --- packages/flutter_hooks/lib/src/hooks.dart | 1 + .../lib/src/overlay_portal_controller.dart | 30 +++++++ .../use_overlay_portal_controller_test.dart | 84 +++++++++++++++++++ 3 files changed, 115 insertions(+) create mode 100644 packages/flutter_hooks/lib/src/overlay_portal_controller.dart create mode 100644 packages/flutter_hooks/test/use_overlay_portal_controller_test.dart diff --git a/packages/flutter_hooks/lib/src/hooks.dart b/packages/flutter_hooks/lib/src/hooks.dart index 023868de..1d840b25 100644 --- a/packages/flutter_hooks/lib/src/hooks.dart +++ b/packages/flutter_hooks/lib/src/hooks.dart @@ -26,6 +26,7 @@ part 'keep_alive.dart'; part 'listenable.dart'; part 'listenable_selector.dart'; part 'misc.dart'; +part 'overlay_portal_controller.dart'; part 'page_controller.dart'; part 'platform_brightness.dart'; part 'primitives.dart'; diff --git a/packages/flutter_hooks/lib/src/overlay_portal_controller.dart b/packages/flutter_hooks/lib/src/overlay_portal_controller.dart new file mode 100644 index 00000000..087ef1c8 --- /dev/null +++ b/packages/flutter_hooks/lib/src/overlay_portal_controller.dart @@ -0,0 +1,30 @@ +part of 'hooks.dart'; + +/// Creates a [OverlayPortalController] that will be disposed automatically. +/// +/// See also: +/// - [OverlayPortalController] +OverlayPortalController useOverlayPortalController({ + List? keys, +}) { + return use(_OverlayPortalControllerHook(keys: keys)); +} + +class _OverlayPortalControllerHook extends Hook { + const _OverlayPortalControllerHook({List? keys}) : super(keys: keys); + + @override + HookState> + createState() => _OverlayPortalControllerHookState(); +} + +class _OverlayPortalControllerHookState + extends HookState { + final controller = OverlayPortalController(); + + @override + OverlayPortalController build(BuildContext context) => controller; + + @override + String get debugLabel => 'useOverlayPortalController'; +} diff --git a/packages/flutter_hooks/test/use_overlay_portal_controller_test.dart b/packages/flutter_hooks/test/use_overlay_portal_controller_test.dart new file mode 100644 index 00000000..30011c4a --- /dev/null +++ b/packages/flutter_hooks/test/use_overlay_portal_controller_test.dart @@ -0,0 +1,84 @@ +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) { + useOverlayPortalController(); + return const SizedBox(); + }), + ); + + await tester.pump(); + + final element = tester.element(find.byType(HookBuilder)); + + expect( + element + .toDiagnosticsNode(style: DiagnosticsTreeStyle.offstage) + .toStringDeep(), + equalsIgnoringHashCodes( + 'HookBuilder\n' + ' │ useOverlayPortalController: OverlayPortalController DETACHED\n' + ' └SizedBox(renderObject: RenderConstrainedBox#00000)\n', + ), + ); + }); + + group('useOverlayPortalController', () { + testWidgets('initial values matches with real constructor', (tester) async { + late OverlayPortalController controller; + final controller2 = OverlayPortalController(); + + await tester.pumpWidget(MaterialApp( + home: Scaffold( + body: HookBuilder(builder: (context) { + controller = useOverlayPortalController(); + return Column( + children: [ + OverlayPortal( + controller: controller, + overlayChildBuilder: (context) => + const Text('Expansion Tile'), + ), + OverlayPortal( + controller: controller2, + overlayChildBuilder: (context) => + const Text('Expansion Tile 2'), + ), + ], + ); + }), + ), + )); + expect(controller, isA()); + expect(controller.isShowing, controller2.isShowing); + }); + + testWidgets('check show/hide of overlay portal', (tester) async { + late OverlayPortalController controller; + await tester.pumpWidget(MaterialApp( + home: Scaffold( + body: HookBuilder(builder: (context) { + controller = useOverlayPortalController(); + return OverlayPortal( + controller: controller, + overlayChildBuilder: (context) => const Text('Expansion Tile 2'), + ); + }), + ), + )); + + expect(controller.isShowing, false); + controller.show(); + expect(controller.isShowing, true); + controller.hide(); + expect(controller.isShowing, false); + }); + }); +} From 9bccb62c54702d1de4612ebdb1790bf616de4a9f Mon Sep 17 00:00:00 2001 From: ume-kun1015 Date: Wed, 11 Dec 2024 00:17:30 +0900 Subject: [PATCH 343/384] docs: updated README and CHANGELOG --- README.md | 1 + packages/flutter_hooks/CHANGELOG.md | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/README.md b/README.md index 99aff235..9106a243 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`. | +| [useOverlayPortalController](https://api.flutter.dev/flutter/widgets/OverlayPortalController-class.html) | Creates a `useOverlayPortalController`. | ## Contributions diff --git a/packages/flutter_hooks/CHANGELOG.md b/packages/flutter_hooks/CHANGELOG.md index 952422b9..a0b28a47 100644 --- a/packages/flutter_hooks/CHANGELOG.md +++ b/packages/flutter_hooks/CHANGELOG.md @@ -1,3 +1,7 @@ +## Unreleased build + +- Added `useOverlayPortalController` (thanks to @offich) + ## 0.21.1-pre.4 - 2024-08-01 - Added `useFixedExtentScrollController` (thanks to @whynotmake-it) From d82c7302d8434dfc8db7212151295bd7f15865aa Mon Sep 17 00:00:00 2001 From: ume-kun1015 Date: Wed, 11 Dec 2024 00:29:14 +0900 Subject: [PATCH 344/384] test: fix texts --- .../test/use_overlay_portal_controller_test.dart | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/flutter_hooks/test/use_overlay_portal_controller_test.dart b/packages/flutter_hooks/test/use_overlay_portal_controller_test.dart index 30011c4a..bbc33e09 100644 --- a/packages/flutter_hooks/test/use_overlay_portal_controller_test.dart +++ b/packages/flutter_hooks/test/use_overlay_portal_controller_test.dart @@ -44,12 +44,12 @@ void main() { OverlayPortal( controller: controller, overlayChildBuilder: (context) => - const Text('Expansion Tile'), + const Text('Overlay Portal'), ), OverlayPortal( controller: controller2, overlayChildBuilder: (context) => - const Text('Expansion Tile 2'), + const Text('Overlay Portal 2'), ), ], ); @@ -68,7 +68,7 @@ void main() { controller = useOverlayPortalController(); return OverlayPortal( controller: controller, - overlayChildBuilder: (context) => const Text('Expansion Tile 2'), + overlayChildBuilder: (context) => const Text('Overlay Portal 2'), ); }), ), From 2a374e985a5ce57c22b7da60bd7baaf6fd7cc57c Mon Sep 17 00:00:00 2001 From: ume-kun1015 Date: Wed, 11 Dec 2024 00:56:48 +0900 Subject: [PATCH 345/384] test: improve test --- .../test/use_overlay_portal_controller_test.dart | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/flutter_hooks/test/use_overlay_portal_controller_test.dart b/packages/flutter_hooks/test/use_overlay_portal_controller_test.dart index bbc33e09..c922645b 100644 --- a/packages/flutter_hooks/test/use_overlay_portal_controller_test.dart +++ b/packages/flutter_hooks/test/use_overlay_portal_controller_test.dart @@ -68,17 +68,24 @@ void main() { controller = useOverlayPortalController(); return OverlayPortal( controller: controller, - overlayChildBuilder: (context) => const Text('Overlay Portal 2'), + overlayChildBuilder: (context) => const Text('Overlay Content'), ); }), ), )); expect(controller.isShowing, false); + expect(find.text('Overlay Content'), findsNothing); + controller.show(); + await tester.pump(); expect(controller.isShowing, true); + expect(find.text('Overlay Content'), findsOneWidget); + controller.hide(); + await tester.pump(); expect(controller.isShowing, false); + expect(find.text('Overlay Content'), findsNothing); }); }); } From 49bd179cd2147f53e65de5c81b3bfd2a89dbc7ee Mon Sep 17 00:00:00 2001 From: offich Date: Sun, 15 Dec 2024 14:10:02 +0900 Subject: [PATCH 346/384] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 9106a243..85253904 100644 --- a/README.md +++ b/README.md @@ -358,7 +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`. | -| [useOverlayPortalController](https://api.flutter.dev/flutter/widgets/OverlayPortalController-class.html) | Creates a `useOverlayPortalController`. | +| [useOverlayPortalController](https://api.flutter.dev/flutter/widgets/OverlayPortalController-class.html) | Creates and manages an `OverlayPortalController` for controlling the visibility of overlay content. The controller will be automatically disposed when no longer needed. | ## Contributions From ba20bb648ea78f7420d6964cc4ff7ae415fcef35 Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Mon, 13 Jan 2025 18:38:40 +0100 Subject: [PATCH 347/384] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 99aff235..ea92f3af 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ [English](https://github.com/rrousselGit/flutter_hooks/blob/master/README.md) | [Português](https://github.com/rrousselGit/flutter_hooks/blob/master/packages/flutter_hooks/resources/translations/pt_br/README.md) | [한국어](https://github.com/rrousselGit/flutter_hooks/blob/master/packages/flutter_hooks/resources/translations/ko_kr/README.md) | [简体中文](https://github.com/rrousselGit/flutter_hooks/blob/master/packages/flutter_hooks/resources/translations/zh_cn/README.md) [![Build](https://github.com/rrousselGit/flutter_hooks/workflows/Build/badge.svg)](https://github.com/rrousselGit/flutter_hooks/actions?query=workflow%3ABuild) [![codecov](https://codecov.io/gh/rrousselGit/flutter_hooks/branch/master/graph/badge.svg)](https://codecov.io/gh/rrousselGit/flutter_hooks) [![pub package](https://img.shields.io/pub/v/flutter_hooks.svg)](https://pub.dev/packages/flutter_hooks) [![pub package](https://img.shields.io/badge/Awesome-Flutter-blue.svg?longCache=true&style=flat-square)](https://github.com/Solido/awesome-flutter) -Discord +Discord From 289f2c39b6d4f28b93757f267ae7937208d47ec7 Mon Sep 17 00:00:00 2001 From: Bent Hillerkus Date: Wed, 19 Feb 2025 14:38:36 +0100 Subject: [PATCH 348/384] feat: add useTreeSliverController --- packages/flutter_hooks/lib/src/hooks.dart | 6 +- .../lib/src/tree_sliver_controller.dart | 28 ++++++++ .../test/use_tree_sliver_controller_test.dart | 68 +++++++++++++++++++ 3 files changed, 100 insertions(+), 2 deletions(-) create mode 100644 packages/flutter_hooks/lib/src/tree_sliver_controller.dart create mode 100644 packages/flutter_hooks/test/use_tree_sliver_controller_test.dart diff --git a/packages/flutter_hooks/lib/src/hooks.dart b/packages/flutter_hooks/lib/src/hooks.dart index 023868de..676fa82f 100644 --- a/packages/flutter_hooks/lib/src/hooks.dart +++ b/packages/flutter_hooks/lib/src/hooks.dart @@ -9,6 +9,7 @@ import 'package:flutter/material.dart' WidgetStatesController, SearchController, TabController, + TreeSliverController, WidgetState; import 'package:flutter/scheduler.dart'; import 'package:flutter/widgets.dart'; @@ -17,6 +18,7 @@ import 'framework.dart'; part 'animation.dart'; part 'async.dart'; +part 'debounced.dart'; part 'draggable_scrollable_controller.dart'; part 'expansion_tile_controller.dart'; part 'fixed_extent_scroll_controller.dart'; @@ -34,6 +36,6 @@ part 'search_controller.dart'; part 'tab_controller.dart'; part 'text_controller.dart'; part 'transformation_controller.dart'; -part 'widgets_binding_observer.dart'; +part 'tree_sliver_controller.dart'; part 'widget_states_controller.dart'; -part 'debounced.dart'; +part 'widgets_binding_observer.dart'; diff --git a/packages/flutter_hooks/lib/src/tree_sliver_controller.dart b/packages/flutter_hooks/lib/src/tree_sliver_controller.dart new file mode 100644 index 00000000..0accf1ab --- /dev/null +++ b/packages/flutter_hooks/lib/src/tree_sliver_controller.dart @@ -0,0 +1,28 @@ +part of 'hooks.dart'; + +/// Creates a [TreeSliverController] that will be disposed automatically. +/// +/// See also: +/// - [TreeSliverController] +TreeSliverController useTreeSliverController() { + return use(const _TreeSliverControllerHook()); +} + +class _TreeSliverControllerHook extends Hook { + const _TreeSliverControllerHook(); + + @override + HookState> createState() => + _TreeSliverControllerHookState(); +} + +class _TreeSliverControllerHookState + extends HookState { + final controller = TreeSliverController(); + + @override + String get debugLabel => 'useTreeSliverController'; + + @override + TreeSliverController build(BuildContext context) => controller; +} diff --git a/packages/flutter_hooks/test/use_tree_sliver_controller_test.dart b/packages/flutter_hooks/test/use_tree_sliver_controller_test.dart new file mode 100644 index 00000000..48646b46 --- /dev/null +++ b/packages/flutter_hooks/test/use_tree_sliver_controller_test.dart @@ -0,0 +1,68 @@ +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) { + useTreeSliverController(); + return const SizedBox(); + }), + ); + + await tester.pump(); + + final element = tester.element(find.byType(HookBuilder)); + + expect( + element + .toDiagnosticsNode(style: DiagnosticsTreeStyle.offstage) + .toStringDeep(), + equalsIgnoringHashCodes( + 'HookBuilder\n' + " │ useTreeSliverController: Instance of 'TreeSliverController'\n" + ' └SizedBox(renderObject: RenderConstrainedBox#00000)\n', + ), + ); + }); + + group('useTreeSliverController', () { + testWidgets('check expansion/collapse of node', (tester) async { + late TreeSliverController controller; + final tree = >[ + TreeSliverNode(0, children: [TreeSliverNode(1), TreeSliverNode(2)]), + TreeSliverNode( + expanded: true, 3, children: [TreeSliverNode(4), TreeSliverNode(5)]) + ]; + await tester.pumpWidget(MaterialApp( + home: Scaffold( + body: HookBuilder(builder: (context) { + controller = useTreeSliverController(); + return CustomScrollView(slivers: [ + TreeSliver( + controller: controller, + tree: tree, + ), + ]); + }), + ), + )); + + expect(controller.isExpanded(tree[0]), false); + controller.expandNode(tree[0]); + expect(controller.isExpanded(tree[0]), true); + controller.collapseNode(tree[0]); + expect(controller.isExpanded(tree[0]), false); + + expect(controller.isExpanded(tree[1]), true); + controller.collapseNode(tree[1]); + expect(controller.isExpanded(tree[1]), false); + controller.expandNode(tree[1]); + expect(controller.isExpanded(tree[1]), true); + }); + }); +} From 08d28d8c22f0c00926c54d30215513f2e3733816 Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Sun, 23 Feb 2025 14:34:03 +0100 Subject: [PATCH 349/384] Discord links --- README.md | 2 +- packages/flutter_hooks/resources/translations/ko_kr/README.md | 2 +- packages/flutter_hooks/resources/translations/zh_cn/README.md | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 99aff235..c4e798a9 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ [English](https://github.com/rrousselGit/flutter_hooks/blob/master/README.md) | [Português](https://github.com/rrousselGit/flutter_hooks/blob/master/packages/flutter_hooks/resources/translations/pt_br/README.md) | [한국어](https://github.com/rrousselGit/flutter_hooks/blob/master/packages/flutter_hooks/resources/translations/ko_kr/README.md) | [简体中文](https://github.com/rrousselGit/flutter_hooks/blob/master/packages/flutter_hooks/resources/translations/zh_cn/README.md) [![Build](https://github.com/rrousselGit/flutter_hooks/workflows/Build/badge.svg)](https://github.com/rrousselGit/flutter_hooks/actions?query=workflow%3ABuild) [![codecov](https://codecov.io/gh/rrousselGit/flutter_hooks/branch/master/graph/badge.svg)](https://codecov.io/gh/rrousselGit/flutter_hooks) [![pub package](https://img.shields.io/pub/v/flutter_hooks.svg)](https://pub.dev/packages/flutter_hooks) [![pub package](https://img.shields.io/badge/Awesome-Flutter-blue.svg?longCache=true&style=flat-square)](https://github.com/Solido/awesome-flutter) -Discord +Discord diff --git a/packages/flutter_hooks/resources/translations/ko_kr/README.md b/packages/flutter_hooks/resources/translations/ko_kr/README.md index 53428f3b..73d9efe9 100644 --- a/packages/flutter_hooks/resources/translations/ko_kr/README.md +++ b/packages/flutter_hooks/resources/translations/ko_kr/README.md @@ -1,7 +1,7 @@ [English](https://github.com/rrousselGit/flutter_hooks/blob/master/README.md) | [Português](https://github.com/rrousselGit/flutter_hooks/blob/master/packages/flutter_hooks/resources/translations/pt_br/README.md) | [한국어](https://github.com/rrousselGit/flutter_hooks/blob/master/packages/flutter_hooks/resources/translations/ko_kr/README.md) | [简体中文](https://github.com/rrousselGit/flutter_hooks/blob/master/packages/flutter_hooks/resources/translations/zh_cn/README.md) [![Build](https://github.com/rrousselGit/flutter_hooks/workflows/Build/badge.svg)](https://github.com/rrousselGit/flutter_hooks/actions?query=workflow%3ABuild) [![codecov](https://codecov.io/gh/rrousselGit/flutter_hooks/branch/master/graph/badge.svg)](https://codecov.io/gh/rrousselGit/flutter_hooks) [![pub package](https://img.shields.io/pub/v/flutter_hooks.svg)](https://pub.dartlang.org/packages/flutter_hooks) [![pub package](https://img.shields.io/badge/Awesome-Flutter-blue.svg?longCache=true&style=flat-square)](https://github.com/Solido/awesome-flutter) -Discord +Discord diff --git a/packages/flutter_hooks/resources/translations/zh_cn/README.md b/packages/flutter_hooks/resources/translations/zh_cn/README.md index e33f13d0..1aa12557 100644 --- a/packages/flutter_hooks/resources/translations/zh_cn/README.md +++ b/packages/flutter_hooks/resources/translations/zh_cn/README.md @@ -2,7 +2,7 @@ [![Build](https://github.com/rrousselGit/flutter_hooks/workflows/Build/badge.svg)](https://github.com/rrousselGit/flutter_hooks/actions?query=workflow%3ABuild) [![codecov](https://codecov.io/gh/rrousselGit/flutter_hooks/branch/master/graph/badge.svg)](https://codecov.io/gh/rrousselGit/flutter_hooks) [![pub package](https://img.shields.io/pub/v/flutter_hooks.svg)](https://pub.dev/packages/flutter_hooks) [![pub package](https://img.shields.io/badge/Awesome-Flutter-blue.svg?longCache=true&style=flat-square)](https://github.com/Solido/awesome-flutter) -Discord +Discord

From 25b7953e65eb67f08b38b864aeee53fc4441321b Mon Sep 17 00:00:00 2001 From: Ryunosuke MURAMATSU Date: Sun, 23 Feb 2025 22:55:50 +0900 Subject: [PATCH 350/384] feat: Add `useCarouselController` (#447) --- README.md | 1 + .../lib/src/carousel_controller.dart | 46 +++++++ packages/flutter_hooks/lib/src/hooks.dart | 2 + .../test/carousel_controller_test.dart | 117 ++++++++++++++++++ 4 files changed, 166 insertions(+) create mode 100644 packages/flutter_hooks/lib/src/carousel_controller.dart create mode 100644 packages/flutter_hooks/test/carousel_controller_test.dart diff --git a/README.md b/README.md index c4e798a9..246d050d 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 00000000..b1ec352c --- /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 023868de..a7ee1e97 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 00000000..b75c90df --- /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'))), + ); + }); + }); +} From 442c252570588675c4bf184943ddec644aae97f0 Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Sun, 23 Feb 2025 14:56:54 +0100 Subject: [PATCH 351/384] Changelog --- packages/flutter_hooks/CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/flutter_hooks/CHANGELOG.md b/packages/flutter_hooks/CHANGELOG.md index 952422b9..11254396 100644 --- a/packages/flutter_hooks/CHANGELOG.md +++ b/packages/flutter_hooks/CHANGELOG.md @@ -1,3 +1,7 @@ +## Unreleased 0.21.2 + +- Add `useCarouselController` (thanks to @riscait) + ## 0.21.1-pre.4 - 2024-08-01 - Added `useFixedExtentScrollController` (thanks to @whynotmake-it) From 06c032a521a93ea8121cdea590db5968f38f0ab4 Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Sun, 23 Feb 2025 14:57:04 +0100 Subject: [PATCH 352/384] flutter_hooks : 0.21.1-pre.4 -> 0.21.2 --- packages/flutter_hooks/CHANGELOG.md | 2 +- packages/flutter_hooks/pubspec.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/flutter_hooks/CHANGELOG.md b/packages/flutter_hooks/CHANGELOG.md index 11254396..f9cd9bb0 100644 --- a/packages/flutter_hooks/CHANGELOG.md +++ b/packages/flutter_hooks/CHANGELOG.md @@ -1,4 +1,4 @@ -## Unreleased 0.21.2 +## 0.21.2 - 2025-02-23 - Add `useCarouselController` (thanks to @riscait) diff --git a/packages/flutter_hooks/pubspec.yaml b/packages/flutter_hooks/pubspec.yaml index 004b7802..a1ab28c2 100644 --- a/packages/flutter_hooks/pubspec.yaml +++ b/packages/flutter_hooks/pubspec.yaml @@ -3,7 +3,7 @@ description: A flutter implementation of React hooks. It adds a new kind of widg homepage: https://github.com/rrousselGit/flutter_hooks repository: https://github.com/rrousselGit/flutter_hooks/tree/master/packages/flutter_hooks issue_tracker: https://github.com/rrousselGit/flutter_hooks/issues -version: 0.21.1-pre.4 +version: 0.21.2 environment: sdk: ">=2.17.0 <3.0.0" From dafb9ebd68397e3344d27c78cbe07c50807c64e3 Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Sun, 23 Feb 2025 14:59:52 +0100 Subject: [PATCH 353/384] Readme --- README.md | 45 +++++++++++++++++++++++---------------------- 1 file changed, 23 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index 246d050d..b2c276b8 100644 --- a/README.md +++ b/README.md @@ -337,28 +337,29 @@ 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`. | -| [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`. | -| [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`. | -| [useCarouselController](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useCarouselController.html) | Creates and disposes a `CarouselController`. | +| 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`. | +| [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`. | +| [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`. | +| [useCarouselController](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useCarouselController.html) | Creates and disposes a `CarouselController`. | +| [useTreeSliverController](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useTreeSliverController.html) | Creates a `TreeSliverController`. | ## Contributions From 4c4aa376dc11519d8342c948674fcfb118297203 Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Sun, 23 Feb 2025 15:00:24 +0100 Subject: [PATCH 354/384] Changelog --- packages/flutter_hooks/CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/flutter_hooks/CHANGELOG.md b/packages/flutter_hooks/CHANGELOG.md index f9cd9bb0..f3fe9e17 100644 --- a/packages/flutter_hooks/CHANGELOG.md +++ b/packages/flutter_hooks/CHANGELOG.md @@ -1,6 +1,7 @@ ## 0.21.2 - 2025-02-23 - Add `useCarouselController` (thanks to @riscait) +- Add `useTreeSliverController` (thanks to @benthillerkus) ## 0.21.1-pre.4 - 2024-08-01 From e1c9a6fea1d801a9a7d7e597aa46c3d362df4821 Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Sun, 23 Feb 2025 15:02:30 +0100 Subject: [PATCH 355/384] Fuse files --- packages/flutter_hooks/lib/src/hooks.dart | 1 - packages/flutter_hooks/lib/src/misc.dart | 29 ++++++++++++++++++ .../lib/src/overlay_portal_controller.dart | 30 ------------------- 3 files changed, 29 insertions(+), 31 deletions(-) delete mode 100644 packages/flutter_hooks/lib/src/overlay_portal_controller.dart diff --git a/packages/flutter_hooks/lib/src/hooks.dart b/packages/flutter_hooks/lib/src/hooks.dart index 1d840b25..023868de 100644 --- a/packages/flutter_hooks/lib/src/hooks.dart +++ b/packages/flutter_hooks/lib/src/hooks.dart @@ -26,7 +26,6 @@ part 'keep_alive.dart'; part 'listenable.dart'; part 'listenable_selector.dart'; part 'misc.dart'; -part 'overlay_portal_controller.dart'; part 'page_controller.dart'; part 'platform_brightness.dart'; part 'primitives.dart'; diff --git a/packages/flutter_hooks/lib/src/misc.dart b/packages/flutter_hooks/lib/src/misc.dart index 147b7651..57510ad3 100644 --- a/packages/flutter_hooks/lib/src/misc.dart +++ b/packages/flutter_hooks/lib/src/misc.dart @@ -229,3 +229,32 @@ class _IsMountedHookState extends HookState { "Use BuildContext.mounted instead if you're on Flutter 3.7.0 or greater", ) typedef IsMounted = bool Function(); + +/// Creates a [OverlayPortalController] that will be disposed automatically. +/// +/// See also: +/// - [OverlayPortalController] +OverlayPortalController useOverlayPortalController({ + List? keys, +}) { + return use(_OverlayPortalControllerHook(keys: keys)); +} + +class _OverlayPortalControllerHook extends Hook { + const _OverlayPortalControllerHook({List? keys}) : super(keys: keys); + + @override + HookState> + createState() => _OverlayPortalControllerHookState(); +} + +class _OverlayPortalControllerHookState + extends HookState { + final controller = OverlayPortalController(); + + @override + OverlayPortalController build(BuildContext context) => controller; + + @override + String get debugLabel => 'useOverlayPortalController'; +} diff --git a/packages/flutter_hooks/lib/src/overlay_portal_controller.dart b/packages/flutter_hooks/lib/src/overlay_portal_controller.dart deleted file mode 100644 index 087ef1c8..00000000 --- a/packages/flutter_hooks/lib/src/overlay_portal_controller.dart +++ /dev/null @@ -1,30 +0,0 @@ -part of 'hooks.dart'; - -/// Creates a [OverlayPortalController] that will be disposed automatically. -/// -/// See also: -/// - [OverlayPortalController] -OverlayPortalController useOverlayPortalController({ - List? keys, -}) { - return use(_OverlayPortalControllerHook(keys: keys)); -} - -class _OverlayPortalControllerHook extends Hook { - const _OverlayPortalControllerHook({List? keys}) : super(keys: keys); - - @override - HookState> - createState() => _OverlayPortalControllerHookState(); -} - -class _OverlayPortalControllerHookState - extends HookState { - final controller = OverlayPortalController(); - - @override - OverlayPortalController build(BuildContext context) => controller; - - @override - String get debugLabel => 'useOverlayPortalController'; -} From 2590ed34236457df2dd9326c6d3cedc9359c3cf2 Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Sun, 23 Feb 2025 15:07:10 +0100 Subject: [PATCH 356/384] Fix --- packages/flutter_hooks/lib/src/hooks.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/flutter_hooks/lib/src/hooks.dart b/packages/flutter_hooks/lib/src/hooks.dart index a3893261..d1e4664a 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, SearchController, From 210bc0a8f030674dcf25ef3e0738176fa08aff4e Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Sun, 23 Feb 2025 15:10:43 +0100 Subject: [PATCH 357/384] Format --- packages/flutter_hooks/lib/src/tab_controller.dart | 6 ++++-- packages/flutter_hooks/test/use_tab_controller_test.dart | 7 +++++-- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/packages/flutter_hooks/lib/src/tab_controller.dart b/packages/flutter_hooks/lib/src/tab_controller.dart index ba40e36f..eef5f0ac 100644 --- a/packages/flutter_hooks/lib/src/tab_controller.dart +++ b/packages/flutter_hooks/lib/src/tab_controller.dart @@ -39,10 +39,12 @@ class _TabControllerHook extends Hook { final Duration? animationDuration; @override - HookState> createState() => _TabControllerHookState(); + HookState> createState() => + _TabControllerHookState(); } -class _TabControllerHookState extends HookState { +class _TabControllerHookState + extends HookState { late final controller = TabController( length: hook.length, initialIndex: hook.initialIndex, diff --git a/packages/flutter_hooks/test/use_tab_controller_test.dart b/packages/flutter_hooks/test/use_tab_controller_test.dart index f93b1641..b4dffce7 100644 --- a/packages/flutter_hooks/test/use_tab_controller_test.dart +++ b/packages/flutter_hooks/test/use_tab_controller_test.dart @@ -20,7 +20,9 @@ void main() { final element = tester.element(find.byType(HookBuilder)); expect( - element.toDiagnosticsNode(style: DiagnosticsTreeStyle.offstage).toStringDeep(), + element + .toDiagnosticsNode(style: DiagnosticsTreeStyle.offstage) + .toStringDeep(), equalsIgnoringHashCodes( 'HookBuilder\n' ' │ useSingleTickerProvider\n' @@ -137,7 +139,8 @@ void main() { ticker.dispose(); }); - testWidgets('initial animationDuration matches with real constructor', (tester) async { + testWidgets('initial animationDuration matches with real constructor', + (tester) async { late TabController controller; late TabController controller2; From 1754ec5f8311eccd834ac7aef26ed318e74461fb Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Sun, 23 Feb 2025 15:16:26 +0100 Subject: [PATCH 358/384] Changelog --- packages/flutter_hooks/CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/flutter_hooks/CHANGELOG.md b/packages/flutter_hooks/CHANGELOG.md index 84ef28fd..7748f78c 100644 --- a/packages/flutter_hooks/CHANGELOG.md +++ b/packages/flutter_hooks/CHANGELOG.md @@ -3,6 +3,7 @@ - Add `useCarouselController` (thanks to @riscait) - Add `useTreeSliverController` (thanks to @benthillerkus) - Added `useOverlayPortalController` (thanks to @offich) +- Add animationDuration property to useTabController hook (thanks to @d-polikhranidi) ## 0.21.1-pre.4 - 2024-08-01 From 5a26c93f404b711e777d323fad830913be6be930 Mon Sep 17 00:00:00 2001 From: Ikko Eltociear Ashimine Date: Sat, 8 Mar 2025 03:50:35 +0900 Subject: [PATCH 359/384] docs: add Japanese README I created Japanese translated README. --- README.md | 2 +- .../resources/translations/ja_jp/README.md | 377 ++++++++++++++++++ .../resources/translations/ko_kr/README.md | 2 +- .../resources/translations/pt_br/README.md | 2 +- .../resources/translations/zh_cn/README.md | 2 +- 5 files changed, 381 insertions(+), 4 deletions(-) create mode 100644 packages/flutter_hooks/resources/translations/ja_jp/README.md diff --git a/README.md b/README.md index bf62334f..1da84d09 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -[English](https://github.com/rrousselGit/flutter_hooks/blob/master/README.md) | [Português](https://github.com/rrousselGit/flutter_hooks/blob/master/packages/flutter_hooks/resources/translations/pt_br/README.md) | [한국어](https://github.com/rrousselGit/flutter_hooks/blob/master/packages/flutter_hooks/resources/translations/ko_kr/README.md) | [简体中文](https://github.com/rrousselGit/flutter_hooks/blob/master/packages/flutter_hooks/resources/translations/zh_cn/README.md) +[English](https://github.com/rrousselGit/flutter_hooks/blob/master/README.md) | [Português](https://github.com/rrousselGit/flutter_hooks/blob/master/packages/flutter_hooks/resources/translations/pt_br/README.md) | [한국어](https://github.com/rrousselGit/flutter_hooks/blob/master/packages/flutter_hooks/resources/translations/ko_kr/README.md) | [简体中文](https://github.com/rrousselGit/flutter_hooks/blob/master/packages/flutter_hooks/resources/translations/zh_cn/README.md) | [日本語](https://github.com/rrousselGit/flutter_hooks/blob/master/packages/flutter_hooks/resources/translations/ja_jp/README.md) [![Build](https://github.com/rrousselGit/flutter_hooks/workflows/Build/badge.svg)](https://github.com/rrousselGit/flutter_hooks/actions?query=workflow%3ABuild) [![codecov](https://codecov.io/gh/rrousselGit/flutter_hooks/branch/master/graph/badge.svg)](https://codecov.io/gh/rrousselGit/flutter_hooks) [![pub package](https://img.shields.io/pub/v/flutter_hooks.svg)](https://pub.dev/packages/flutter_hooks) [![pub package](https://img.shields.io/badge/Awesome-Flutter-blue.svg?longCache=true&style=flat-square)](https://github.com/Solido/awesome-flutter) Discord diff --git a/packages/flutter_hooks/resources/translations/ja_jp/README.md b/packages/flutter_hooks/resources/translations/ja_jp/README.md new file mode 100644 index 00000000..efbdcdcf --- /dev/null +++ b/packages/flutter_hooks/resources/translations/ja_jp/README.md @@ -0,0 +1,377 @@ +[English](https://github.com/rrousselGit/flutter_hooks/blob/master/README.md) | [Português](https://github.com/rrousselGit/flutter_hooks/blob/master/packages/flutter_hooks/resources/translations/pt_br/README.md) | [한국어](https://github.com/rrousselGit/flutter_hooks/blob/master/packages/flutter_hooks/resources/translations/ko_kr/README.md) | [简体中文](https://github.com/rrousselGit/flutter_hooks/blob/master/packages/flutter_hooks/resources/translations/zh_cn/README.md) | [日本語](https://github.com/rrousselGit/flutter_hooks/blob/master/packages/flutter_hooks/resources/translations/ja_jp/README.md) + +[![Build](https://github.com/rrousselGit/flutter_hooks/workflows/Build/badge.svg)](https://github.com/rrousselGit/flutter_hooks/actions?query=workflow%3ABuild) [![codecov](https://codecov.io/gh/rrousselGit/flutter_hooks/branch/master/graph/badge.svg)](https://codecov.io/gh/rrousselGit/flutter_hooks) [![pub package](https://img.shields.io/pub/v/flutter_hooks.svg)](https://pub.dev/packages/flutter_hooks) [![pub package](https://img.shields.io/badge/Awesome-Flutter-blue.svg?longCache=true&style=flat-square)](https://github.com/Solido/awesome-flutter) +Discord + + + +# Flutter Hooks + +React hooksのFlutter実装: https://medium.com/@dan_abramov/making-sense-of-react-hooks-fdbde8803889 + +Hooksは、`Widget`のライフサイクルを管理する新しい種類のオブジェクトです。これらは、ウィジェット間のコード共有を増やし、重複を排除するために存在します。 + +## 動機 + +`StatefulWidget`には大きな問題があります。それは、`initState`や`dispose`のロジックを再利用するのが非常に難しいことです。明らかな例は`AnimationController`です: + +```dart +class Example extends StatefulWidget { + const Example({super.key, required this.duration}); + + final Duration duration; + + @override + _ExampleState createState() => _ExampleState(); +} + +class _ExampleState extends State with SingleTickerProviderStateMixin { + late final AnimationController _controller; + + @override + void initState() { + super.initState(); + _controller = AnimationController(vsync: this, duration: widget.duration); + } + + @override + void didUpdateWidget(Example oldWidget) { + super.didUpdateWidget(oldWidget); + if (widget.duration != oldWidget.duration) { + _controller.duration = widget.duration; + } + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Container(); + } +} +``` + +`AnimationController`を使用したいすべてのウィジェットは、このロジックのほとんどを最初から再実装する必要があります。これはもちろん望ましくありません。 + +Dartのミックスインはこの問題を部分的に解決できますが、他の問題も抱えています: + +- 特定のミックスインはクラスごとに1回しか使用できません。 +- ミックスインとクラスは同じオブジェクトを共有します。\ + これは、2つのミックスインが同じ名前の変数を定義した場合、結果がコンパイルエラーから未知の動作までさまざまであることを意味します。 + +--- + +このライブラリは第三の解決策を提案します: + +```dart +class Example extends HookWidget { + const Example({super.key, required this.duration}); + + final Duration duration; + + @override + Widget build(BuildContext context) { + final controller = useAnimationController(duration: duration); + return Container(); + } +} +``` + +このコードは前の例と機能的に同等です。`AnimationController`をdisposeし、`Example.duration`が変更されたときにその`duration`を更新します。 +しかし、あなたはおそらくこう思っているでしょう: + +> すべてのロジックはどこに行ったのですか? + +そのロジックは、このライブラリに直接含まれている関数`useAnimationController`に移動されました([既存のフック](https://github.com/rrousselGit/flutter_hooks#existing-hooks)を参照)。これが私たちが呼ぶ_Hook_です。 + +Hooksは、いくつかの特性を持つ新しい種類のオブジェクトです: + +- それらは、`Hooks`をミックスインしたウィジェットの`build`メソッドでのみ使用できます。 +- 同じフックを任意の回数再利用できます。 + 次のコードは、2つの独立した`AnimationController`を定義し、ウィジェットが再構築されるときに正しく保持されます。 + + ```dart + Widget build(BuildContext context) { + final controller = useAnimationController(); + final controller2 = useAnimationController(); + return Container(); + } + ``` + +- フックは互いに完全に独立しており、ウィジェットからも独立しています。\ + これは、それらを簡単にパッケージに抽出し、他の人が使用できるように[pub](https://pub.dev/)に公開できることを意味します。 + +## 原則 + +`State`と同様に、フックは`Widget`の`Element`に保存されます。ただし、1つの`State`を持つ代わりに、`Element`は`List`を保存します。次に、`Hook`を使用するためには、`Hook.use`を呼び出す必要があります。 + +`use`によって返されるフックは、呼び出された回数に基づいています。 +最初の呼び出しは最初のフックを返し、2回目の呼び出しは2番目のフックを返し、3回目の呼び出しは3番目のフックを返します。 + +このアイデアがまだ不明確な場合、フックの単純な実装は次のようになります: + +```dart +class HookElement extends Element { + List _hooks; + int _hookIndex; + + T use(Hook hook) => _hooks[_hookIndex++].build(this); + + @override + performRebuild() { + _hookIndex = 0; + super.performRebuild(); + } +} +``` + +フックがどのように実装されているかについての詳細な説明については、Reactでの実装方法に関する素晴らしい記事があります:https://medium.com/@ryardley/react-hooks-not-magic-just-arrays-cd4f1857236e + +## 規則 + +フックがそのインデックスから取得されるため、いくつかの規則を守る必要があります: + +### フックの名前を常に`use`で始める: + +```dart +Widget build(BuildContext context) { + // `use`で始まる、良い名前 + useMyHook(); + // `use`で始まらない、これはフックではないと誤解される可能性があります + myHook(); + // .... +} +``` + +### フックを無条件に呼び出す + +```dart +Widget build(BuildContext context) { + useMyHook(); + // .... +} +``` + +### `use`を条件にラップしない + +```dart +Widget build(BuildContext context) { + if (condition) { + useMyHook(); + } + // .... +} +``` + +--- + +### ホットリロードについて + +フックがそのインデックスから取得されるため、リファクタリング中のホットリロードがアプリケーションを壊すと考えるかもしれません。 + +しかし心配しないでください、`HookWidget`はフックで動作するようにデフォルトのホットリロード動作をオーバーライドします。それでも、フックの状態がリセットされる場合があります。 + +次のフックのリストを考えてみましょう: + +```dart +useA(); +useB(0); +useC(); +``` + +次に、ホットリロードを実行した後に`HookB`のパラメータを編集したと考えます: + +```dart +useA(); +useB(42); +useC(); +``` + +ここではすべてが正常に動作し、すべてのフックがその状態を保持します。 + +次に、`HookB`を削除したと考えます。次のようになります: + +```dart +useA(); +useC(); +``` + +この場合、`HookA`はその状態を保持しますが、`HookC`はハードリセットされます。 +これは、リファクタリング後にホットリロードが実行されると、最初の影響を受けた行の後のすべてのフックがdisposeされるためです。 +したがって、`HookC`が`HookB`の後に配置されていたため、disposeされます。 + +## フックの作成方法 + +フックを作成する方法は2つあります: + +- 関数 + + 関数はフックを書く最も一般的な方法です。フックが本質的に合成可能であるため、関数は他のフックを組み合わせてより複雑なカスタムフックを作成できます。慣例として、これらの関数は`use`で始まります。 + + 次のコードは、変数を作成し、その値が変更されるたびにコンソールにログを記録するカスタムフックを定義します: + + ```dart + ValueNotifier useLoggedState([T initialData]) { + final result = useState(initialData); + useValueChanged(result.value, (_, __) { + print(result.value); + }); + return result; + } + ``` + +- クラス + + フックが複雑すぎる場合は、`Hook`を拡張するクラスに変換することができます。これを使用して`Hook.use`を使用できます。\ + クラスとして、フックは`State`クラスと非常に似ており、ウィジェットのライフサイクルや`initHook`、`dispose`、`setState`などのメソッドにアクセスできます。 + + 通常、クラスを次のように関数の下に隠すのが良いプラクティスです: + + ```dart + Result useMyHook() { + return use(const _TimeAlive()); + } + ``` + + 次のコードは、`State`が生存していた合計時間をそのdispose時に出力するフックを定義します。 + + ```dart + class _TimeAlive extends Hook { + const _TimeAlive(); + + @override + _TimeAliveState createState() => _TimeAliveState(); + } + + class _TimeAliveState extends HookState { + DateTime start; + + @override + void initHook() { + super.initHook(); + start = DateTime.now(); + } + + @override + void build(BuildContext context) {} + + @override + void dispose() { + print(DateTime.now().difference(start)); + super.dispose(); + } + } + ``` + +## 既存のフック + +Flutter_Hooksには、再利用可能なフックのリストが既に含まれており、さまざまな種類に分かれています: + +### プリミティブ + +ウィジェットのさまざまなライフサイクルと対話する低レベルのフックのセット + +| 名前 | 説明 | +| -------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------- | +| [useEffect](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useEffect.html) | 副作用に役立ち、オプションでそれらをキャンセルします。 | +| [useState](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useState.html) | 変数を作成し、それを購読します。 | +| [useMemoized](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useMemoized.html) | 複雑なオブジェクトのインスタンスをキャッシュします。 | +| [useRef](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useRef.html) | 単一の可変プロパティを含むオブジェクトを作成します。 | +| [useCallback](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useCallback.html) | 関数インスタンスをキャッシュします。 | +| [useContext](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useContext.html) | ビルド中の`HookWidget`の`BuildContext`を取得します。 | +| [useValueChanged](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useValueChanged.html) | 値を監視し、その値が変更されるたびにコールバックをトリガーします。 | + +### オブジェクトバインディング + +このカテゴリのフックは、既存のFlutter/Dartオブジェクトをフックで操作します。 +それらはオブジェクトの作成/更新/破棄を担当します。 + +#### dart:async関連のフック: + +| 名前 | 説明 | +| ---------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------- | +| [useStream](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useStream.html) | `Stream`を購読し、その現在の状態を`AsyncSnapshot`として返します。 | +| [useStreamController](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useStreamController.html) | 自動的に破棄される`StreamController`を作成します。 | +| [useOnStreamChange](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useOnStreamChange.html) | `Stream`を購読し、ハンドラを登録し、`StreamSubscription`を返します。 | +| [useFuture](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useFuture.html) | `Future`を購読し、その現在の状態を`AsyncSnapshot`として返します。 | + +#### アニメーション関連のフック: + +| 名前 | 説明 | +| ------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------- | +| [useSingleTickerProvider](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useSingleTickerProvider.html) | 単一使用の`TickerProvider`を作成します。 | +| [useAnimationController](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useAnimationController.html) | 自動的に破棄される`AnimationController`を作成します。 | +| [useAnimation](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useAnimation.html) | `Animation`を購読し、その値を返します。 | + +#### Listenable関連のフック: + +| 名前 | 説明 | +| -------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------- | +| [useListenable](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useListenable.html) | `Listenable`を購読し、リスナーが呼び出されるたびにウィジェットをビルドが必要なものとしてマークします。 | +| [useListenableSelector](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useListenableSelector.html) | `useListenable`に似ていますが、UIの再構築をフィルタリングできます。 | +| [useValueNotifier](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useValueNotifier.html) | 自動的に破棄される`ValueNotifier`を作成します。 | +| [useValueListenable](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useValueListenable.html) | `ValueListenable`を購読し、その値を返します。 | +| [useOnListenableChange](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useOnListenableChange.html) | 指定されたリスナーコールバックを`Listenable`に追加し、自動的に削除されます。 | + +#### その他のフック: + +特定のテーマを持たない一連のフック。 + +| 名前 | 説明 | +| -------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| [useReducer](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useReducer.html) | より複雑な状態のための`useState`の代替。 | +| [usePrevious](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/usePrevious.html) | [usePrevious]に渡された前の引数を返します。 | +| [useTextEditingController](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useTextEditingController-constant.html) | `TextEditingController`を作成します。 | +| [useFocusNode](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useFocusNode.html) | `FocusNode`を作成します。 | +| [useTabController](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useTabController.html) | `TabController`を作成し、破棄します。 | +| [useScrollController](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useScrollController.html) | `ScrollController`を作成し、破棄します。 | +| [usePageController](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/usePageController.html) | `PageController`を作成し、破棄します。 | +| [useFixedExtentScrollController](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useFixedExtentScrollController.html) | `FixedExtentScrollController`を作成し、破棄します。 | +| [useAppLifecycleState](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useAppLifecycleState.html) | 現在の`AppLifecycleState`を返し、変更時にウィジェットを再構築します。 | +| [useOnAppLifecycleStateChange](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useOnAppLifecycleStateChange.html) | `AppLifecycleState`の変更を監視し、変更時にコールバックをトリガーします。 | +| [useTransformationController](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useTransformationController.html) | `TransformationController`を作成し、破棄します。 | +| [useIsMounted](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useIsMounted.html) | フックのための`State.mounted`の同等物。 | +| [useAutomaticKeepAlive](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useAutomaticKeepAlive.html) | フックのための`AutomaticKeepAlive`ウィジェットの同等物。 | +| [useOnPlatformBrightnessChange](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useOnPlatformBrightnessChange.html) | プラットフォームの`Brightness`の変更を監視し、変更時にコールバックをトリガーします。 | +| [useSearchController](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useSearchController.html) | `SearchController`を作成し、破棄します。 | +| [useWidgetStatesController](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useWidgetStatesController.html) | `WidgetStatesController`を作成し、破棄します。 | +| [useExpansionTileController](https://api.flutter.dev/flutter/material/ExpansionTileController-class.html) | `ExpansionTileController`を作成します。 | +| [useDebounced](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useDebounced.html) | 指定されたタイムアウト期間後にウィジェットの更新をトリガーする、提供された値のデバウンスバージョンを返します。 | +| [useDraggableScrollableController](https://api.flutter.dev/flutter/widgets/DraggableScrollableController-class.html) | `DraggableScrollableController`を作成します。 | +| [useCarouselController](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useCarouselController.html) | **`CarouselController`**を作成し、破棄します。 | +| [useTreeSliverController](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useTreeSliverController.html) | `TreeSliverController`を作成します。 | +| [useOverlayPortalController](https://api.flutter.dev/flutter/widgets/OverlayPortalController-class.html) | オーバーレイコンテンツの表示を制御するための`OverlayPortalController`を作成および管理します。コントローラーは、不要になったときに自動的に破棄されます。 | + +## 貢献 + +貢献は歓迎されます! + +フックが不足していると感じた場合は、プルリクエストを開いてください。 + +カスタムフックをマージするには、次のことを行う必要があります: + +- 使用例を説明します。 + + このフックがなぜ必要なのか、どのように使用するのかを説明する問題を開きます。... + これは重要です。フックが多くの人にアピールしない場合、そのフックはマージされません。 + + フックが拒否された場合でも心配しないでください!拒否されたからといって、将来的により多くの人が関心を示した場合にマージされないわけではありません。 + その間、https://pub.devにフックをパッケージとして公開してください。 + +- フックのテストを書く + + フックが将来誤って壊れるのを防ぐために、完全にテストされない限り、フックはマージされません。 + +- READMEに追加し、そのためのドキュメントを書く。 + +## スポンサー + +

+ + + +

diff --git a/packages/flutter_hooks/resources/translations/ko_kr/README.md b/packages/flutter_hooks/resources/translations/ko_kr/README.md index 73d9efe9..d815a99e 100644 --- a/packages/flutter_hooks/resources/translations/ko_kr/README.md +++ b/packages/flutter_hooks/resources/translations/ko_kr/README.md @@ -1,4 +1,4 @@ -[English](https://github.com/rrousselGit/flutter_hooks/blob/master/README.md) | [Português](https://github.com/rrousselGit/flutter_hooks/blob/master/packages/flutter_hooks/resources/translations/pt_br/README.md) | [한국어](https://github.com/rrousselGit/flutter_hooks/blob/master/packages/flutter_hooks/resources/translations/ko_kr/README.md) | [简体中文](https://github.com/rrousselGit/flutter_hooks/blob/master/packages/flutter_hooks/resources/translations/zh_cn/README.md) +[English](https://github.com/rrousselGit/flutter_hooks/blob/master/README.md) | [Português](https://github.com/rrousselGit/flutter_hooks/blob/master/packages/flutter_hooks/resources/translations/pt_br/README.md) | [한국어](https://github.com/rrousselGit/flutter_hooks/blob/master/packages/flutter_hooks/resources/translations/ko_kr/README.md) | [简体中文](https://github.com/rrousselGit/flutter_hooks/blob/master/packages/flutter_hooks/resources/translations/zh_cn/README.md) | [日本語](https://github.com/rrousselGit/flutter_hooks/blob/master/packages/flutter_hooks/resources/translations/ja_jp/README.md) [![Build](https://github.com/rrousselGit/flutter_hooks/workflows/Build/badge.svg)](https://github.com/rrousselGit/flutter_hooks/actions?query=workflow%3ABuild) [![codecov](https://codecov.io/gh/rrousselGit/flutter_hooks/branch/master/graph/badge.svg)](https://codecov.io/gh/rrousselGit/flutter_hooks) [![pub package](https://img.shields.io/pub/v/flutter_hooks.svg)](https://pub.dartlang.org/packages/flutter_hooks) [![pub package](https://img.shields.io/badge/Awesome-Flutter-blue.svg?longCache=true&style=flat-square)](https://github.com/Solido/awesome-flutter) Discord diff --git a/packages/flutter_hooks/resources/translations/pt_br/README.md b/packages/flutter_hooks/resources/translations/pt_br/README.md index 761fdf9b..a804cdee 100644 --- a/packages/flutter_hooks/resources/translations/pt_br/README.md +++ b/packages/flutter_hooks/resources/translations/pt_br/README.md @@ -1,4 +1,4 @@ -[English](https://github.com/rrousselGit/flutter_hooks/blob/master/README.md) | [Português](https://github.com/rrousselGit/flutter_hooks/blob/master/packages/flutter_hooks/resources/translations/pt_br/README.md) | [한국어](https://github.com/rrousselGit/flutter_hooks/blob/master/packages/flutter_hooks/resources/translations/ko_kr/README.md) | [简体中文](https://github.com/rrousselGit/flutter_hooks/blob/master/packages/flutter_hooks/resources/translations/zh_cn/README.md) +[English](https://github.com/rrousselGit/flutter_hooks/blob/master/README.md) | [Português](https://github.com/rrousselGit/flutter_hooks/blob/master/packages/flutter_hooks/resources/translations/pt_br/README.md) | [한국어](https://github.com/rrousselGit/flutter_hooks/blob/master/packages/flutter_hooks/resources/translations/ko_kr/README.md) | [简体中文](https://github.com/rrousselGit/flutter_hooks/blob/master/packages/flutter_hooks/resources/translations/zh_cn/README.md) | [日本語](https://github.com/rrousselGit/flutter_hooks/blob/master/packages/flutter_hooks/resources/translations/ja_jp/README.md) [![Build Status](https://travis-ci.org/rrousselGit/flutter_hooks.svg?branch=master)](https://travis-ci.org/rrousselGit/flutter_hooks) [![codecov](https://codecov.io/gh/rrousselGit/flutter_hooks/branch/master/graph/badge.svg)](https://codecov.io/gh/rrousselGit/flutter_hooks) [![pub package](https://img.shields.io/pub/v/flutter_hooks.svg)](https://pub.dartlang.org/packages/flutter_hooks) [![pub package](https://img.shields.io/badge/Awesome-Flutter-blue.svg?longCache=true&style=flat-square)](https://github.com/Solido/awesome-flutter) diff --git a/packages/flutter_hooks/resources/translations/zh_cn/README.md b/packages/flutter_hooks/resources/translations/zh_cn/README.md index 1aa12557..4e3c7e6e 100644 --- a/packages/flutter_hooks/resources/translations/zh_cn/README.md +++ b/packages/flutter_hooks/resources/translations/zh_cn/README.md @@ -1,4 +1,4 @@ -[English](https://github.com/rrousselGit/flutter_hooks/blob/master/README.md) | [Português](https://github.com/rrousselGit/flutter_hooks/blob/master/packages/flutter_hooks/resources/translations/pt_br/README.md) | [한국어](https://github.com/rrousselGit/flutter_hooks/blob/master/packages/flutter_hooks/resources/translations/ko_kr/README.md) | [简体中文](https://github.com/rrousselGit/flutter_hooks/blob/master/packages/flutter_hooks/resources/translations/zh_cn/README.md) +[English](https://github.com/rrousselGit/flutter_hooks/blob/master/README.md) | [Português](https://github.com/rrousselGit/flutter_hooks/blob/master/packages/flutter_hooks/resources/translations/pt_br/README.md) | [한국어](https://github.com/rrousselGit/flutter_hooks/blob/master/packages/flutter_hooks/resources/translations/ko_kr/README.md) | [简体中文](https://github.com/rrousselGit/flutter_hooks/blob/master/packages/flutter_hooks/resources/translations/zh_cn/README.md) | [日本語](https://github.com/rrousselGit/flutter_hooks/blob/master/packages/flutter_hooks/resources/translations/ja_jp/README.md) [![Build](https://github.com/rrousselGit/flutter_hooks/workflows/Build/badge.svg)](https://github.com/rrousselGit/flutter_hooks/actions?query=workflow%3ABuild) [![codecov](https://codecov.io/gh/rrousselGit/flutter_hooks/branch/master/graph/badge.svg)](https://codecov.io/gh/rrousselGit/flutter_hooks) [![pub package](https://img.shields.io/pub/v/flutter_hooks.svg)](https://pub.dev/packages/flutter_hooks) [![pub package](https://img.shields.io/badge/Awesome-Flutter-blue.svg?longCache=true&style=flat-square)](https://github.com/Solido/awesome-flutter) From 44d23b7760eb2badd63fdba380bb15f2e3687529 Mon Sep 17 00:00:00 2001 From: Bent Hillerkus <29630575+benthillerkus@users.noreply.github.com> Date: Sat, 22 Mar 2025 00:53:32 +0100 Subject: [PATCH 360/384] feat: add Hook for managing SnapshotController instances --- packages/flutter_hooks/lib/src/hooks.dart | 1 + .../lib/src/snapshot_controller.dart | 61 +++++++++++++++ .../test/use_snapshot_controller_test.dart | 75 +++++++++++++++++++ 3 files changed, 137 insertions(+) create mode 100644 packages/flutter_hooks/lib/src/snapshot_controller.dart create mode 100644 packages/flutter_hooks/test/use_snapshot_controller_test.dart diff --git a/packages/flutter_hooks/lib/src/hooks.dart b/packages/flutter_hooks/lib/src/hooks.dart index d1e4664a..b5e1d562 100644 --- a/packages/flutter_hooks/lib/src/hooks.dart +++ b/packages/flutter_hooks/lib/src/hooks.dart @@ -41,3 +41,4 @@ part 'transformation_controller.dart'; part 'tree_sliver_controller.dart'; part 'widget_states_controller.dart'; part 'widgets_binding_observer.dart'; +part 'snapshot_controller.dart'; \ No newline at end of file diff --git a/packages/flutter_hooks/lib/src/snapshot_controller.dart b/packages/flutter_hooks/lib/src/snapshot_controller.dart new file mode 100644 index 00000000..9fcf70ab --- /dev/null +++ b/packages/flutter_hooks/lib/src/snapshot_controller.dart @@ -0,0 +1,61 @@ +part of 'hooks.dart'; + +/// Creates and disposes a [SnapshotController]. +/// +/// Note that [allowSnapshotting] must be set to `true` +/// in order for this controller to actually do anything. +/// This is consistent with [SnapshotController.new]. +/// +/// If [allowSnapshotting] changes on subsequent calls to [useSnapshotController], +/// [SnapshotController.allowSnapshotting] will be called to update accordingly. +/// +/// ```dart +/// final controller = useSnapshotController(allowSnapshotting: true); +/// // is equivalent to +/// final controller = useSnapshotController(); +/// controller.allowSnapshotting = true; +/// ``` +/// +/// See also: +/// - [SnapshotController] +SnapshotController useSnapshotController({ + bool allowSnapshotting = false, +}) { + return use( + _SnapshotControllerHook( + allowSnapshotting: allowSnapshotting, + ), + ); +} + +class _SnapshotControllerHook extends Hook { + const _SnapshotControllerHook({ + required this.allowSnapshotting, + }); + + final bool allowSnapshotting; + + @override + HookState> + createState() => _SnapshotControllerHookState(); +} + +class _SnapshotControllerHookState + extends HookState { + late final controller = SnapshotController(allowSnapshotting: hook.allowSnapshotting); + + @override + void didUpdateHook(_SnapshotControllerHook oldHook) { + super.didUpdateHook(oldHook); + controller.allowSnapshotting = hook.allowSnapshotting; + } + + @override + SnapshotController build(BuildContext context) => controller; + + @override + void dispose() => controller.dispose(); + + @override + String get debugLabel => 'useSnapshotController'; +} diff --git a/packages/flutter_hooks/test/use_snapshot_controller_test.dart b/packages/flutter_hooks/test/use_snapshot_controller_test.dart new file mode 100644 index 00000000..842691d4 --- /dev/null +++ b/packages/flutter_hooks/test/use_snapshot_controller_test.dart @@ -0,0 +1,75 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/widgets.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) { + useSnapshotController(); + return const SizedBox(); + }), + ); + + await tester.pump(); + + final element = tester.element(find.byType(HookBuilder)); + + expect( + element + .toDiagnosticsNode(style: DiagnosticsTreeStyle.offstage) + .toStringDeep(), + equalsIgnoringHashCodes( + 'HookBuilder\n' + " │ useSnapshotController: Instance of 'SnapshotController'\n" + ' └SizedBox(renderObject: RenderConstrainedBox#00000)\n', + ), + ); + }); + + group('useSnapshotController', () { + testWidgets('initial values matches with real constructor', (tester) async { + late SnapshotController controller; + late SnapshotController controller2; + + await tester.pumpWidget( + HookBuilder(builder: (context) { + controller2 = SnapshotController(); + controller = useSnapshotController(); + return Container(); + }), + ); + + expect(controller.allowSnapshotting, controller2.allowSnapshotting); + }); + + testWidgets('passes hook parameters to the SnapshotController', + (tester) async { + late SnapshotController controller; + + await tester.pumpWidget( + HookBuilder(builder: (context) { + controller = useSnapshotController(allowSnapshotting: true); + return const SizedBox(); + }), + ); + + expect(controller.allowSnapshotting, true); + + late SnapshotController retrievedController; + await tester.pumpWidget( + HookBuilder(builder: (context) { + // ignore: avoid_redundant_argument_values + retrievedController = useSnapshotController(allowSnapshotting: false); + return const SizedBox(); + }), + ); + + expect(retrievedController, same(controller)); + expect(retrievedController.allowSnapshotting, false); + }); + }); +} From 48e4d3a93a00c449d9a9563d7e24c0305deb9630 Mon Sep 17 00:00:00 2001 From: Bent Hillerkus <29630575+benthillerkus@users.noreply.github.com> Date: Sat, 22 Mar 2025 01:16:13 +0100 Subject: [PATCH 361/384] chore: autoformat --- packages/flutter_hooks/lib/src/hooks.dart | 2 +- .../flutter_hooks/lib/src/snapshot_controller.dart | 13 +++++++------ 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/packages/flutter_hooks/lib/src/hooks.dart b/packages/flutter_hooks/lib/src/hooks.dart index b5e1d562..2ad681b1 100644 --- a/packages/flutter_hooks/lib/src/hooks.dart +++ b/packages/flutter_hooks/lib/src/hooks.dart @@ -41,4 +41,4 @@ part 'transformation_controller.dart'; part 'tree_sliver_controller.dart'; part 'widget_states_controller.dart'; part 'widgets_binding_observer.dart'; -part 'snapshot_controller.dart'; \ No newline at end of file +part 'snapshot_controller.dart'; diff --git a/packages/flutter_hooks/lib/src/snapshot_controller.dart b/packages/flutter_hooks/lib/src/snapshot_controller.dart index 9fcf70ab..d5b69ec7 100644 --- a/packages/flutter_hooks/lib/src/snapshot_controller.dart +++ b/packages/flutter_hooks/lib/src/snapshot_controller.dart @@ -1,14 +1,14 @@ part of 'hooks.dart'; /// Creates and disposes a [SnapshotController]. -/// +/// /// Note that [allowSnapshotting] must be set to `true` /// in order for this controller to actually do anything. /// This is consistent with [SnapshotController.new]. -/// +/// /// If [allowSnapshotting] changes on subsequent calls to [useSnapshotController], /// [SnapshotController.allowSnapshotting] will be called to update accordingly. -/// +/// /// ```dart /// final controller = useSnapshotController(allowSnapshotting: true); /// // is equivalent to @@ -36,13 +36,14 @@ class _SnapshotControllerHook extends Hook { final bool allowSnapshotting; @override - HookState> - createState() => _SnapshotControllerHookState(); + HookState> createState() => + _SnapshotControllerHookState(); } class _SnapshotControllerHookState extends HookState { - late final controller = SnapshotController(allowSnapshotting: hook.allowSnapshotting); + late final controller = + SnapshotController(allowSnapshotting: hook.allowSnapshotting); @override void didUpdateHook(_SnapshotControllerHook oldHook) { From a86a8801f9fefd21191b09610e7c274003fd82fe Mon Sep 17 00:00:00 2001 From: Bent Hillerkus <29630575+benthillerkus@users.noreply.github.com> Date: Sat, 22 Mar 2025 03:00:03 +0100 Subject: [PATCH 362/384] ci: run on stable --- .github/workflows/build.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c8bfff11..f94be497 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -17,6 +17,7 @@ jobs: - flutter_hooks channel: - master + - stable steps: - uses: actions/checkout@v2 @@ -30,11 +31,12 @@ jobs: - name: Check format run: dart format --set-exit-if-changed . - if: matrix.channel == 'master' + if: matrix.channel == 'stable' working-directory: packages/${{ matrix.package }} - name: Analyze run: dart analyze . + if: matrix.channel == 'stable' working-directory: packages/${{ matrix.package }} - name: Run tests From 32af7899aedac6e1aa09b1f1755ebf41ef4af680 Mon Sep 17 00:00:00 2001 From: Bent Hillerkus <29630575+benthillerkus@users.noreply.github.com> Date: Sat, 22 Mar 2025 03:11:11 +0100 Subject: [PATCH 363/384] fix: only upload code coverage results on stable branch --- .github/workflows/build.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f94be497..e922857d 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -45,4 +45,5 @@ jobs: - name: Upload coverage to codecov run: curl -s https://codecov.io/bash | bash + if: matrix.channel == 'stable' working-directory: packages/${{ matrix.package }} From 002d859ae2389d5c6094a1c101dcb2570990a469 Mon Sep 17 00:00:00 2001 From: Bent Hillerkus <29630575+benthillerkus@users.noreply.github.com> Date: Sat, 22 Mar 2025 03:17:43 +0100 Subject: [PATCH 364/384] ci: disable fail-fast in build workflow --- .github/workflows/build.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e922857d..8cfe46a4 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -18,6 +18,7 @@ jobs: channel: - master - stable + fail-fast: false steps: - uses: actions/checkout@v2 From 2b099d9084a80b90d9cad21de4931e5e72d3b304 Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Mon, 2 Jun 2025 17:03:03 +0200 Subject: [PATCH 365/384] Various lints --- packages/flutter_hooks/lib/src/misc.dart | 38 +++++++++---------- .../test/use_animation_controller_test.dart | 9 +++-- .../test/use_ticker_provider_test.dart | 8 +++- 3 files changed, 31 insertions(+), 24 deletions(-) diff --git a/packages/flutter_hooks/lib/src/misc.dart b/packages/flutter_hooks/lib/src/misc.dart index 57510ad3..b461ce3d 100644 --- a/packages/flutter_hooks/lib/src/misc.dart +++ b/packages/flutter_hooks/lib/src/misc.dart @@ -1,17 +1,17 @@ part of 'hooks.dart'; /// A store of mutable state that allows mutations by dispatching actions. -abstract class Store { +abstract class Store { /// The current state. /// /// This value may change after a call to [dispatch]. - State get state; + StateT get state; /// Dispatches an action. /// /// Actions are dispatched synchronously. /// It is impossible to try to dispatch actions during `build`. - void dispatch(Action action); + void dispatch(ActionT action); } /// Composes an [Action] and a [State] to create a new [State]. @@ -33,10 +33,10 @@ typedef Reducer = State Function(State state, Action action); /// See also: /// * [Reducer] /// * [Store] -Store useReducer( - Reducer reducer, { - required State initialState, - required Action initialAction, +Store useReducer( + Reducer reducer, { + required StateT initialState, + required ActionT initialAction, }) { return use( _ReducerHook( @@ -47,27 +47,27 @@ Store useReducer( ); } -class _ReducerHook extends Hook> { +class _ReducerHook extends Hook> { const _ReducerHook( this.reducer, { required this.initialState, required this.initialAction, }); - final Reducer reducer; - final State initialState; - final Action initialAction; + final Reducer reducer; + final StateT initialState; + final ActionT initialAction; @override - _ReducerHookState createState() => - _ReducerHookState(); + _ReducerHookState createState() => + _ReducerHookState(); } -class _ReducerHookState - extends HookState, _ReducerHook> - implements Store { +class _ReducerHookState + extends HookState, _ReducerHook> + implements Store { @override - late State state = hook.reducer(hook.initialState, hook.initialAction); + late StateT state = hook.reducer(hook.initialState, hook.initialAction); @override void initHook() { @@ -77,7 +77,7 @@ class _ReducerHookState } @override - void dispatch(Action action) { + void dispatch(ActionT action) { final newState = hook.reducer(state, action); if (state != newState) { @@ -86,7 +86,7 @@ class _ReducerHookState } @override - Store build(BuildContext context) { + Store build(BuildContext context) { return this; } diff --git a/packages/flutter_hooks/test/use_animation_controller_test.dart b/packages/flutter_hooks/test/use_animation_controller_test.dart index bf766aae..2c71bf32 100644 --- a/packages/flutter_hooks/test/use_animation_controller_test.dart +++ b/packages/flutter_hooks/test/use_animation_controller_test.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:flutter/foundation.dart'; import 'package:flutter/scheduler.dart'; import 'package:flutter/widgets.dart'; @@ -26,9 +28,10 @@ void main() { controller ..duration = const Duration(seconds: 1) - ..reverseDuration = const Duration(seconds: 1) - // check has a ticker - ..forward(); + ..reverseDuration = const Duration(seconds: 1); + + // check has a ticker + unawaited(controller.forward()); // dispose await tester.pumpWidget(const SizedBox()); diff --git a/packages/flutter_hooks/test/use_ticker_provider_test.dart b/packages/flutter_hooks/test/use_ticker_provider_test.dart index 329fea41..801a8b7b 100644 --- a/packages/flutter_hooks/test/use_ticker_provider_test.dart +++ b/packages/flutter_hooks/test/use_ticker_provider_test.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; @@ -41,8 +43,10 @@ void main() { )); final animationController = AnimationController( - vsync: provider, duration: const Duration(seconds: 1)) - ..forward(); + vsync: provider, + duration: const Duration(seconds: 1), + ); + unawaited(animationController.forward()); expect(() => AnimationController(vsync: provider), throwsFlutterError); From 5cffacb86c0dabc35599929007041ea1d3389457 Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Mon, 2 Jun 2025 17:25:31 +0200 Subject: [PATCH 366/384] Lints --- README.md | 4 +-- packages/flutter_hooks/CHANGELOG.md | 4 +++ .../lib/src/expansion_tile_controller.dart | 29 ++++++++++++------- packages/flutter_hooks/lib/src/hooks.dart | 1 + packages/flutter_hooks/pubspec.yaml | 2 +- ...rt => use_expansible_controller_test.dart} | 18 ++++++------ 6 files changed, 36 insertions(+), 22 deletions(-) rename packages/flutter_hooks/test/{use_expansion_tile_controller_test.dart => use_expansible_controller_test.dart} (80%) diff --git a/README.md b/README.md index bf62334f..8d7255e1 100644 --- a/README.md +++ b/README.md @@ -355,10 +355,10 @@ A series of hooks with no particular theme. | [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`. | +| [useExpansibleController](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useExpansibleController.html) | Creates a `ExpansibleController`. | | [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`**. | +| [useCarouselController](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useCarouselController.html) | Creates and disposes a **`CarouselController`**. | | [useTreeSliverController](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useTreeSliverController.html) | Creates a `TreeSliverController`. | | [useOverlayPortalController](https://api.flutter.dev/flutter/widgets/OverlayPortalController-class.html) | Creates and manages an `OverlayPortalController` for controlling the visibility of overlay content. The controller will be automatically disposed when no longer needed. | diff --git a/packages/flutter_hooks/CHANGELOG.md b/packages/flutter_hooks/CHANGELOG.md index 7748f78c..ed4a35d1 100644 --- a/packages/flutter_hooks/CHANGELOG.md +++ b/packages/flutter_hooks/CHANGELOG.md @@ -1,3 +1,7 @@ +## Unreleased patch + +Deprecated `useExpansionTileController` in favor of `useExpansibleController`. + ## 0.21.2 - 2025-02-23 - Add `useCarouselController` (thanks to @riscait) diff --git a/packages/flutter_hooks/lib/src/expansion_tile_controller.dart b/packages/flutter_hooks/lib/src/expansion_tile_controller.dart index f698f54b..665aef49 100644 --- a/packages/flutter_hooks/lib/src/expansion_tile_controller.dart +++ b/packages/flutter_hooks/lib/src/expansion_tile_controller.dart @@ -1,28 +1,37 @@ part of 'hooks.dart'; +/// Creates a [ExpansibleController] that will be disposed automatically. +/// +/// See also: +/// - [ExpansibleController] +ExpansibleController useExpansibleController({List? keys}) { + return use(_ExpansibleControllerHook(keys: keys)); +} + /// Creates a [ExpansionTileController] that will be disposed automatically. /// /// See also: /// - [ExpansionTileController] +@Deprecated('Use `useExpansibleController` instead.') ExpansionTileController useExpansionTileController({List? keys}) { - return use(_ExpansionTileControllerHook(keys: keys)); + return use(_ExpansibleControllerHook(keys: keys)); } -class _ExpansionTileControllerHook extends Hook { - const _ExpansionTileControllerHook({List? keys}) : super(keys: keys); +class _ExpansibleControllerHook extends Hook { + const _ExpansibleControllerHook({List? keys}) : super(keys: keys); @override - HookState> - createState() => _ExpansionTileControllerHookState(); + HookState> createState() => + _ExpansibleControllerHookState(); } -class _ExpansionTileControllerHookState - extends HookState { - final controller = ExpansionTileController(); +class _ExpansibleControllerHookState + extends HookState { + final controller = ExpansibleController(); @override - String get debugLabel => 'useExpansionTileController'; + String get debugLabel => 'useExpansibleController'; @override - ExpansionTileController build(BuildContext context) => controller; + ExpansibleController build(BuildContext context) => controller; } diff --git a/packages/flutter_hooks/lib/src/hooks.dart b/packages/flutter_hooks/lib/src/hooks.dart index d1e4664a..7fb94cd6 100644 --- a/packages/flutter_hooks/lib/src/hooks.dart +++ b/packages/flutter_hooks/lib/src/hooks.dart @@ -6,6 +6,7 @@ import 'package:flutter/material.dart' Brightness, CarouselController, DraggableScrollableController, + // ignore: deprecated_member_use ExpansionTileController, SearchController, TabController, diff --git a/packages/flutter_hooks/pubspec.yaml b/packages/flutter_hooks/pubspec.yaml index a1ab28c2..ae237b0d 100644 --- a/packages/flutter_hooks/pubspec.yaml +++ b/packages/flutter_hooks/pubspec.yaml @@ -7,7 +7,7 @@ version: 0.21.2 environment: sdk: ">=2.17.0 <3.0.0" - flutter: ">=3.21.0-13.0.pre.4" + flutter: ">=3.32.0" dependencies: flutter: diff --git a/packages/flutter_hooks/test/use_expansion_tile_controller_test.dart b/packages/flutter_hooks/test/use_expansible_controller_test.dart similarity index 80% rename from packages/flutter_hooks/test/use_expansion_tile_controller_test.dart rename to packages/flutter_hooks/test/use_expansible_controller_test.dart index 84dd57d1..76d2207d 100644 --- a/packages/flutter_hooks/test/use_expansion_tile_controller_test.dart +++ b/packages/flutter_hooks/test/use_expansible_controller_test.dart @@ -9,7 +9,7 @@ void main() { testWidgets('debugFillProperties', (tester) async { await tester.pumpWidget( HookBuilder(builder: (context) { - useExpansionTileController(); + useExpansibleController(); return const SizedBox(); }), ); @@ -24,21 +24,21 @@ void main() { .toStringDeep(), equalsIgnoringHashCodes( 'HookBuilder\n' - " │ useExpansionTileController: Instance of 'ExpansionTileController'\n" + " │ useExpansibleController: Instance of 'ExpansibleController'\n" ' └SizedBox(renderObject: RenderConstrainedBox#00000)\n', ), ); }); - group('useExpansionTileController', () { + group('useExpansibleController', () { testWidgets('initial values matches with real constructor', (tester) async { - late ExpansionTileController controller; - final controller2 = ExpansionTileController(); + late ExpansibleController controller; + final controller2 = ExpansibleController(); await tester.pumpWidget(MaterialApp( home: Scaffold( body: HookBuilder(builder: (context) { - controller = useExpansionTileController(); + controller = useExpansibleController(); return Column( children: [ ExpansionTile( @@ -54,16 +54,16 @@ void main() { }), ), )); - expect(controller, isA()); + expect(controller, isA()); expect(controller.isExpanded, controller2.isExpanded); }); testWidgets('check expansion/collapse of tile', (tester) async { - late ExpansionTileController controller; + late ExpansibleController controller; await tester.pumpWidget(MaterialApp( home: Scaffold( body: HookBuilder(builder: (context) { - controller = useExpansionTileController(); + controller = useExpansibleController(); return ExpansionTile( controller: controller, title: const Text('Expansion Tile'), From dad3d0eb4dce2a90eb2357d49b36b69133d3c0f7 Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Mon, 2 Jun 2025 17:31:06 +0200 Subject: [PATCH 367/384] Fix master --- .../use_transformation_controller_test.dart | 30 +++++++++++++------ 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/packages/flutter_hooks/test/use_transformation_controller_test.dart b/packages/flutter_hooks/test/use_transformation_controller_test.dart index 4df11c08..c67a0368 100644 --- a/packages/flutter_hooks/test/use_transformation_controller_test.dart +++ b/packages/flutter_hooks/test/use_transformation_controller_test.dart @@ -20,15 +20,27 @@ void main() { element .toDiagnosticsNode(style: DiagnosticsTreeStyle.offstage) .toStringDeep(), - equalsIgnoringHashCodes( - 'HookBuilder\n' - ' │ useTransformationController:\n' - ' │ TransformationController#00000([0] 1.0,0.0,0.0,0.0\n' - ' │ [1] 0.0,1.0,0.0,0.0\n' - ' │ [2] 0.0,0.0,1.0,0.0\n' - ' │ [3] 0.0,0.0,0.0,1.0\n' - ' │ )\n' - ' └SizedBox(renderObject: RenderConstrainedBox#00000)\n', + anyOf( + equalsIgnoringHashCodes( + 'HookBuilder\n' + ' │ useTransformationController:\n' + ' │ TransformationController#00000([0] 1.0,0.0,0.0,0.0\n' + ' │ [1] 0.0,1.0,0.0,0.0\n' + ' │ [2] 0.0,0.0,1.0,0.0\n' + ' │ [3] 0.0,0.0,0.0,1.0\n' + ' │ )\n' + ' └SizedBox(renderObject: RenderConstrainedBox#00000)\n', + ), + equalsIgnoringHashCodes( + 'HookBuilder\n' + ' │ useTransformationController:\n' + ' │ TransformationController#00000([0] [1.0,0.0,0.0,0.0]\n' + ' │ [1] [0.0,1.0,0.0,0.0]\n' + ' │ [2] [0.0,0.0,1.0,0.0]\n' + ' │ [3] [0.0,0.0,0.0,1.0]\n' + ' │ )\n' + ' └SizedBox(renderObject: RenderConstrainedBox#00000)\n', + ), ), ); }); From 60c65268bbe4ac88f7cdef28e457a2f43ccf12e7 Mon Sep 17 00:00:00 2001 From: Eren Gun Date: Tue, 17 Jun 2025 09:43:46 +0300 Subject: [PATCH 368/384] Add useCupertinoTabController hook for automatic disposal of CupertinoTabController --- .../lib/src/cupertino_tab_controller.dart | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 packages/flutter_hooks/lib/src/cupertino_tab_controller.dart diff --git a/packages/flutter_hooks/lib/src/cupertino_tab_controller.dart b/packages/flutter_hooks/lib/src/cupertino_tab_controller.dart new file mode 100644 index 00000000..eac6af51 --- /dev/null +++ b/packages/flutter_hooks/lib/src/cupertino_tab_controller.dart @@ -0,0 +1,47 @@ +part of 'hooks.dart'; + + +/// Creates a [CupertinoTabController] that will be disposed automatically. +/// +/// See also: +/// - [CupertinoTabController] +CupertinoTabController useCupertinoTabController({ + int initialIndex = 0, + List? keys, +}) { + return use( + _CupertinoTabControllerHook( + initialIndex: initialIndex, + keys: keys, + ), + ); +} + +class _CupertinoTabControllerHook extends Hook { + const _CupertinoTabControllerHook({ + required this.initialIndex, + super.keys, + }); + + final int initialIndex; + + @override + HookState> createState() => + _CupertinoTabControllerHookState(); +} + +class _CupertinoTabControllerHookState + extends HookState { + late final controller = CupertinoTabController( + initialIndex: hook.initialIndex, + ); + + @override + CupertinoTabController build(BuildContext context) => controller; + + @override + void dispose() => controller.dispose(); + + @override + String get debugLabel => 'useCupertinoTabController'; +} \ No newline at end of file From afa5a6ed5257d01471a3dfa92b73545805c6d43e Mon Sep 17 00:00:00 2001 From: Eren Gun Date: Tue, 17 Jun 2025 09:43:51 +0300 Subject: [PATCH 369/384] Add import for CupertinoTabController and include it in part directives --- packages/flutter_hooks/lib/src/hooks.dart | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/flutter_hooks/lib/src/hooks.dart b/packages/flutter_hooks/lib/src/hooks.dart index 7fb94cd6..9c8ffa3c 100644 --- a/packages/flutter_hooks/lib/src/hooks.dart +++ b/packages/flutter_hooks/lib/src/hooks.dart @@ -1,5 +1,7 @@ import 'dart:async'; +import 'package:flutter/cupertino.dart' + show CupertinoTabController; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart' show @@ -21,6 +23,7 @@ import 'framework.dart'; part 'animation.dart'; part 'async.dart'; part 'carousel_controller.dart'; +part 'cupertino_tab_controller.dart'; part 'debounced.dart'; part 'draggable_scrollable_controller.dart'; part 'expansion_tile_controller.dart'; From 70e78ddeda005db5c521cf8f9caab08620a5c3fd Mon Sep 17 00:00:00 2001 From: Eren Gun Date: Tue, 17 Jun 2025 09:43:58 +0300 Subject: [PATCH 370/384] Add tests for useCupertinoTabController functionality --- .../use_cupertino_tab_controller_test.dart | 89 +++++++++++++++++++ 1 file changed, 89 insertions(+) create mode 100644 packages/flutter_hooks/test/use_cupertino_tab_controller_test.dart diff --git a/packages/flutter_hooks/test/use_cupertino_tab_controller_test.dart b/packages/flutter_hooks/test/use_cupertino_tab_controller_test.dart new file mode 100644 index 00000000..727956ff --- /dev/null +++ b/packages/flutter_hooks/test/use_cupertino_tab_controller_test.dart @@ -0,0 +1,89 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/foundation.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) { + useCupertinoTabController(); + return const SizedBox(); + }), + ); + + await tester.pump(); + + final element = tester.element(find.byType(HookBuilder)); + + expect( + element + .toDiagnosticsNode(style: DiagnosticsTreeStyle.offstage) + .toStringDeep(), + equalsIgnoringHashCodes( + 'HookBuilder\n' + " │ useCupertinoTabController: Instance of 'CupertinoTabController'\n" + ' └SizedBox(renderObject: RenderConstrainedBox#00000)\n', + ), + ); + }); + + group('useCupertinoCupertinoCupertinoTabController', () { + testWidgets('initial values matches with real constructor', (tester) async { + late CupertinoTabController controller; + late CupertinoTabController controller2; + + await tester.pumpWidget( + HookBuilder(builder: (context) { + controller2 = CupertinoTabController(); + controller = useCupertinoTabController(); + return Container(); + }), + ); + + expect(controller.index, controller2.index); + }); + testWidgets("returns a CupertinoTabController that doesn't change", + (tester) async { + late CupertinoTabController controller; + late CupertinoTabController controller2; + + await tester.pumpWidget( + HookBuilder(builder: (context) { + controller = useCupertinoTabController(initialIndex: 1); + return Container(); + }), + ); + + expect(controller, isA()); + + await tester.pumpWidget( + HookBuilder(builder: (context) { + controller2 = useCupertinoTabController(initialIndex: 1); + return Container(); + }), + ); + + expect(identical(controller, controller2), isTrue); + }); + + testWidgets('passes hook parameters to the CupertinoTabController', + (tester) async { + late CupertinoTabController controller; + + await tester.pumpWidget( + HookBuilder( + builder: (context) { + controller = useCupertinoTabController(initialIndex: 2); + + return Container(); + }, + ), + ); + + expect(controller.index, 2); + }); + }); +} From 4e8e00c1391ca759090fce9af36ebfa9b264388b Mon Sep 17 00:00:00 2001 From: Eren Gun Date: Tue, 17 Jun 2025 10:48:19 +0300 Subject: [PATCH 371/384] Fix typo in test group name for useCupertinoTabController --- .../flutter_hooks/test/use_cupertino_tab_controller_test.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/flutter_hooks/test/use_cupertino_tab_controller_test.dart b/packages/flutter_hooks/test/use_cupertino_tab_controller_test.dart index 727956ff..a57748a2 100644 --- a/packages/flutter_hooks/test/use_cupertino_tab_controller_test.dart +++ b/packages/flutter_hooks/test/use_cupertino_tab_controller_test.dart @@ -30,7 +30,7 @@ void main() { ); }); - group('useCupertinoCupertinoCupertinoTabController', () { + group('useCupertinoTabController', () { testWidgets('initial values matches with real constructor', (tester) async { late CupertinoTabController controller; late CupertinoTabController controller2; From 85c1827e60787decb9278063e980662bee489485 Mon Sep 17 00:00:00 2001 From: Gabber235 Date: Wed, 6 Aug 2025 05:03:45 +0200 Subject: [PATCH 372/384] feat: add descendantsAreTraversable parameter to useFocusNode --- packages/flutter_hooks/lib/src/focus_node.dart | 6 ++++++ packages/flutter_hooks/test/use_focus_node_test.dart | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/packages/flutter_hooks/lib/src/focus_node.dart b/packages/flutter_hooks/lib/src/focus_node.dart index 5dfb3e06..ef41915b 100644 --- a/packages/flutter_hooks/lib/src/focus_node.dart +++ b/packages/flutter_hooks/lib/src/focus_node.dart @@ -10,6 +10,7 @@ FocusNode useFocusNode({ bool skipTraversal = false, bool canRequestFocus = true, bool descendantsAreFocusable = true, + bool descendantsAreTraversable = true, }) { return use( _FocusNodeHook( @@ -18,6 +19,7 @@ FocusNode useFocusNode({ skipTraversal: skipTraversal, canRequestFocus: canRequestFocus, descendantsAreFocusable: descendantsAreFocusable, + descendantsAreTraversable: descendantsAreTraversable, ), ); } @@ -29,6 +31,7 @@ class _FocusNodeHook extends Hook { required this.skipTraversal, required this.canRequestFocus, required this.descendantsAreFocusable, + required this.descendantsAreTraversable, }); final String? debugLabel; @@ -36,6 +39,7 @@ class _FocusNodeHook extends Hook { final bool skipTraversal; final bool canRequestFocus; final bool descendantsAreFocusable; + final bool descendantsAreTraversable; @override _FocusNodeHookState createState() { @@ -50,6 +54,7 @@ class _FocusNodeHookState extends HookState { skipTraversal: hook.skipTraversal, canRequestFocus: hook.canRequestFocus, descendantsAreFocusable: hook.descendantsAreFocusable, + descendantsAreTraversable: hook.descendantsAreTraversable, ); @override @@ -59,6 +64,7 @@ class _FocusNodeHookState extends HookState { ..skipTraversal = hook.skipTraversal ..canRequestFocus = hook.canRequestFocus ..descendantsAreFocusable = hook.descendantsAreFocusable + ..descendantsAreTraversable = hook.descendantsAreTraversable ..onKeyEvent = hook.onKeyEvent; } diff --git a/packages/flutter_hooks/test/use_focus_node_test.dart b/packages/flutter_hooks/test/use_focus_node_test.dart index 055893ec..0942c6b9 100644 --- a/packages/flutter_hooks/test/use_focus_node_test.dart +++ b/packages/flutter_hooks/test/use_focus_node_test.dart @@ -76,6 +76,8 @@ void main() { expect(focusNode.skipTraversal, official.skipTraversal); expect(focusNode.canRequestFocus, official.canRequestFocus); expect(focusNode.descendantsAreFocusable, official.descendantsAreFocusable); + expect(focusNode.descendantsAreTraversable, + official.descendantsAreTraversable); }); testWidgets('has all the FocusNode parameters', (tester) async { @@ -91,6 +93,7 @@ void main() { skipTraversal: true, canRequestFocus: false, descendantsAreFocusable: false, + descendantsAreTraversable: false, ); return Container(); }), @@ -101,6 +104,7 @@ void main() { expect(focusNode.skipTraversal, true); expect(focusNode.canRequestFocus, false); expect(focusNode.descendantsAreFocusable, false); + expect(focusNode.descendantsAreTraversable, false); }); testWidgets('handles parameter change', (tester) async { @@ -118,6 +122,7 @@ void main() { skipTraversal: true, canRequestFocus: false, descendantsAreFocusable: false, + descendantsAreTraversable: false, ); return Container(); @@ -140,5 +145,6 @@ void main() { expect(focusNode.skipTraversal, false); expect(focusNode.canRequestFocus, true); expect(focusNode.descendantsAreFocusable, true); + expect(focusNode.descendantsAreTraversable, true); }); } From c76490a69aae9107f490ff9706c4924b3eba6890 Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Tue, 12 Aug 2025 18:20:05 +0200 Subject: [PATCH 373/384] Docs --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index fcb293cd..4f014f86 100644 --- a/README.md +++ b/README.md @@ -355,12 +355,13 @@ A series of hooks with no particular theme. | [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`. | -| [useExpansibleController](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useExpansibleController.html) | Creates a `ExpansibleController`. | +| [useExpansibleController](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useExpansibleController.html) | Creates a `ExpansibleController`. | | [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`**. | | [useTreeSliverController](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useTreeSliverController.html) | Creates a `TreeSliverController`. | | [useOverlayPortalController](https://api.flutter.dev/flutter/widgets/OverlayPortalController-class.html) | Creates and manages an `OverlayPortalController` for controlling the visibility of overlay content. The controller will be automatically disposed when no longer needed. | +| [useSnapshotController](https://api.flutter.dev/flutter/widgets/SnapshotController-class.html) | Creates and manages a `SnapshotController` | ## Contributions From 94ca1e5a98d2df3da9c2a40ad703697ebed1cbc4 Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Tue, 12 Aug 2025 18:32:09 +0200 Subject: [PATCH 374/384] Doc --- README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 4f014f86..a50587e2 100644 --- a/README.md +++ b/README.md @@ -360,8 +360,9 @@ A series of hooks with no particular theme. | [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`**. | | [useTreeSliverController](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useTreeSliverController.html) | Creates a `TreeSliverController`. | -| [useOverlayPortalController](https://api.flutter.dev/flutter/widgets/OverlayPortalController-class.html) | Creates and manages an `OverlayPortalController` for controlling the visibility of overlay content. The controller will be automatically disposed when no longer needed. | -| [useSnapshotController](https://api.flutter.dev/flutter/widgets/SnapshotController-class.html) | Creates and manages a `SnapshotController` | +| [useOverlayPortalController](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useOverlayPortalController.html) | Creates and manages an `OverlayPortalController` for controlling the visibility of overlay content. The controller will be automatically disposed when no longer needed. | +| [useSnapshotController](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useSnapshotController.html) | Creates and manages a `SnapshotController` | +| [useCupertinoController](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useCupertinoController.html) | Creates and manages a `CupertinoController` | ## Contributions From 2e3d75d2dbf8e3fc5809827e8f3311ac941abb99 Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Tue, 12 Aug 2025 18:47:08 +0200 Subject: [PATCH 375/384] Format --- .../flutter_hooks/lib/src/cupertino_tab_controller.dart | 7 +++---- packages/flutter_hooks/lib/src/hooks.dart | 3 +-- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/packages/flutter_hooks/lib/src/cupertino_tab_controller.dart b/packages/flutter_hooks/lib/src/cupertino_tab_controller.dart index eac6af51..53e71140 100644 --- a/packages/flutter_hooks/lib/src/cupertino_tab_controller.dart +++ b/packages/flutter_hooks/lib/src/cupertino_tab_controller.dart @@ -1,6 +1,5 @@ part of 'hooks.dart'; - /// Creates a [CupertinoTabController] that will be disposed automatically. /// /// See also: @@ -26,8 +25,8 @@ class _CupertinoTabControllerHook extends Hook { final int initialIndex; @override - HookState> createState() => - _CupertinoTabControllerHookState(); + HookState> + createState() => _CupertinoTabControllerHookState(); } class _CupertinoTabControllerHookState @@ -44,4 +43,4 @@ class _CupertinoTabControllerHookState @override String get debugLabel => 'useCupertinoTabController'; -} \ No newline at end of file +} diff --git a/packages/flutter_hooks/lib/src/hooks.dart b/packages/flutter_hooks/lib/src/hooks.dart index e149f429..f9b04a9b 100644 --- a/packages/flutter_hooks/lib/src/hooks.dart +++ b/packages/flutter_hooks/lib/src/hooks.dart @@ -1,7 +1,6 @@ import 'dart:async'; -import 'package:flutter/cupertino.dart' - show CupertinoTabController; +import 'package:flutter/cupertino.dart' show CupertinoTabController; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart' show From 7d0c41ae10376f891519e18d6358f6efe832c5c5 Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Tue, 12 Aug 2025 18:50:51 +0200 Subject: [PATCH 376/384] Changelog --- packages/flutter_hooks/CHANGELOG.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/flutter_hooks/CHANGELOG.md b/packages/flutter_hooks/CHANGELOG.md index ed4a35d1..b65d03e1 100644 --- a/packages/flutter_hooks/CHANGELOG.md +++ b/packages/flutter_hooks/CHANGELOG.md @@ -1,6 +1,8 @@ ## Unreleased patch -Deprecated `useExpansionTileController` in favor of `useExpansibleController`. +- Deprecated `useExpansionTileController` in favor of `useExpansibleController`. +- Added `useCupertinoTabController` thanks to @Erengun +- Added `useSnapshotController` thanks to @benthillerkus ## 0.21.2 - 2025-02-23 From aec406e74e4cf5cae529625e779e7f4b209f3756 Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Tue, 12 Aug 2025 18:52:01 +0200 Subject: [PATCH 377/384] Changelog --- packages/flutter_hooks/CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/flutter_hooks/CHANGELOG.md b/packages/flutter_hooks/CHANGELOG.md index b65d03e1..07cbd414 100644 --- a/packages/flutter_hooks/CHANGELOG.md +++ b/packages/flutter_hooks/CHANGELOG.md @@ -3,6 +3,7 @@ - Deprecated `useExpansionTileController` in favor of `useExpansibleController`. - Added `useCupertinoTabController` thanks to @Erengun - Added `useSnapshotController` thanks to @benthillerkus +- Added `descendantsAreTraversable` to `useFocusNode` thanks to @gabber235 ## 0.21.2 - 2025-02-23 From b97f6bb36f59809b195462933a495dc5d87108d4 Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Tue, 12 Aug 2025 18:55:59 +0200 Subject: [PATCH 378/384] flutter_hooks : 0.21.2 -> 0.21.3 --- packages/flutter_hooks/CHANGELOG.md | 2 +- packages/flutter_hooks/pubspec.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/flutter_hooks/CHANGELOG.md b/packages/flutter_hooks/CHANGELOG.md index 07cbd414..1fd75f4f 100644 --- a/packages/flutter_hooks/CHANGELOG.md +++ b/packages/flutter_hooks/CHANGELOG.md @@ -1,4 +1,4 @@ -## Unreleased patch +## 0.21.3 - 2025-08-12 - Deprecated `useExpansionTileController` in favor of `useExpansibleController`. - Added `useCupertinoTabController` thanks to @Erengun diff --git a/packages/flutter_hooks/pubspec.yaml b/packages/flutter_hooks/pubspec.yaml index ae237b0d..f42a2010 100644 --- a/packages/flutter_hooks/pubspec.yaml +++ b/packages/flutter_hooks/pubspec.yaml @@ -3,7 +3,7 @@ description: A flutter implementation of React hooks. It adds a new kind of widg homepage: https://github.com/rrousselGit/flutter_hooks repository: https://github.com/rrousselGit/flutter_hooks/tree/master/packages/flutter_hooks issue_tracker: https://github.com/rrousselGit/flutter_hooks/issues -version: 0.21.2 +version: 0.21.3 environment: sdk: ">=2.17.0 <3.0.0" From f96d647f4712c754a761f21fb4301c3d1c039074 Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Tue, 19 Aug 2025 14:55:14 +0200 Subject: [PATCH 379/384] Update Discord link --- README.md | 2 +- packages/flutter_hooks/CHANGELOG.md | 4 ++++ packages/flutter_hooks/resources/translations/ja_jp/README.md | 2 +- packages/flutter_hooks/resources/translations/ko_kr/README.md | 2 +- packages/flutter_hooks/resources/translations/zh_cn/README.md | 2 +- 5 files changed, 8 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index a50587e2..ee67f958 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ [English](https://github.com/rrousselGit/flutter_hooks/blob/master/README.md) | [Português](https://github.com/rrousselGit/flutter_hooks/blob/master/packages/flutter_hooks/resources/translations/pt_br/README.md) | [한국어](https://github.com/rrousselGit/flutter_hooks/blob/master/packages/flutter_hooks/resources/translations/ko_kr/README.md) | [简体中文](https://github.com/rrousselGit/flutter_hooks/blob/master/packages/flutter_hooks/resources/translations/zh_cn/README.md) | [日本語](https://github.com/rrousselGit/flutter_hooks/blob/master/packages/flutter_hooks/resources/translations/ja_jp/README.md) [![Build](https://github.com/rrousselGit/flutter_hooks/workflows/Build/badge.svg)](https://github.com/rrousselGit/flutter_hooks/actions?query=workflow%3ABuild) [![codecov](https://codecov.io/gh/rrousselGit/flutter_hooks/branch/master/graph/badge.svg)](https://codecov.io/gh/rrousselGit/flutter_hooks) [![pub package](https://img.shields.io/pub/v/flutter_hooks.svg)](https://pub.dev/packages/flutter_hooks) [![pub package](https://img.shields.io/badge/Awesome-Flutter-blue.svg?longCache=true&style=flat-square)](https://github.com/Solido/awesome-flutter) -Discord +Discord diff --git a/packages/flutter_hooks/CHANGELOG.md b/packages/flutter_hooks/CHANGELOG.md index 1fd75f4f..74a48e57 100644 --- a/packages/flutter_hooks/CHANGELOG.md +++ b/packages/flutter_hooks/CHANGELOG.md @@ -1,3 +1,7 @@ +## Unreleased 0.21.3+1 + +Updated Discord link + ## 0.21.3 - 2025-08-12 - Deprecated `useExpansionTileController` in favor of `useExpansibleController`. diff --git a/packages/flutter_hooks/resources/translations/ja_jp/README.md b/packages/flutter_hooks/resources/translations/ja_jp/README.md index efbdcdcf..98e015ef 100644 --- a/packages/flutter_hooks/resources/translations/ja_jp/README.md +++ b/packages/flutter_hooks/resources/translations/ja_jp/README.md @@ -1,7 +1,7 @@ [English](https://github.com/rrousselGit/flutter_hooks/blob/master/README.md) | [Português](https://github.com/rrousselGit/flutter_hooks/blob/master/packages/flutter_hooks/resources/translations/pt_br/README.md) | [한국어](https://github.com/rrousselGit/flutter_hooks/blob/master/packages/flutter_hooks/resources/translations/ko_kr/README.md) | [简体中文](https://github.com/rrousselGit/flutter_hooks/blob/master/packages/flutter_hooks/resources/translations/zh_cn/README.md) | [日本語](https://github.com/rrousselGit/flutter_hooks/blob/master/packages/flutter_hooks/resources/translations/ja_jp/README.md) [![Build](https://github.com/rrousselGit/flutter_hooks/workflows/Build/badge.svg)](https://github.com/rrousselGit/flutter_hooks/actions?query=workflow%3ABuild) [![codecov](https://codecov.io/gh/rrousselGit/flutter_hooks/branch/master/graph/badge.svg)](https://codecov.io/gh/rrousselGit/flutter_hooks) [![pub package](https://img.shields.io/pub/v/flutter_hooks.svg)](https://pub.dev/packages/flutter_hooks) [![pub package](https://img.shields.io/badge/Awesome-Flutter-blue.svg?longCache=true&style=flat-square)](https://github.com/Solido/awesome-flutter) -Discord +Discord diff --git a/packages/flutter_hooks/resources/translations/ko_kr/README.md b/packages/flutter_hooks/resources/translations/ko_kr/README.md index d815a99e..112dd7e2 100644 --- a/packages/flutter_hooks/resources/translations/ko_kr/README.md +++ b/packages/flutter_hooks/resources/translations/ko_kr/README.md @@ -1,7 +1,7 @@ [English](https://github.com/rrousselGit/flutter_hooks/blob/master/README.md) | [Português](https://github.com/rrousselGit/flutter_hooks/blob/master/packages/flutter_hooks/resources/translations/pt_br/README.md) | [한국어](https://github.com/rrousselGit/flutter_hooks/blob/master/packages/flutter_hooks/resources/translations/ko_kr/README.md) | [简体中文](https://github.com/rrousselGit/flutter_hooks/blob/master/packages/flutter_hooks/resources/translations/zh_cn/README.md) | [日本語](https://github.com/rrousselGit/flutter_hooks/blob/master/packages/flutter_hooks/resources/translations/ja_jp/README.md) [![Build](https://github.com/rrousselGit/flutter_hooks/workflows/Build/badge.svg)](https://github.com/rrousselGit/flutter_hooks/actions?query=workflow%3ABuild) [![codecov](https://codecov.io/gh/rrousselGit/flutter_hooks/branch/master/graph/badge.svg)](https://codecov.io/gh/rrousselGit/flutter_hooks) [![pub package](https://img.shields.io/pub/v/flutter_hooks.svg)](https://pub.dartlang.org/packages/flutter_hooks) [![pub package](https://img.shields.io/badge/Awesome-Flutter-blue.svg?longCache=true&style=flat-square)](https://github.com/Solido/awesome-flutter) -Discord +Discord diff --git a/packages/flutter_hooks/resources/translations/zh_cn/README.md b/packages/flutter_hooks/resources/translations/zh_cn/README.md index 4e3c7e6e..9410be60 100644 --- a/packages/flutter_hooks/resources/translations/zh_cn/README.md +++ b/packages/flutter_hooks/resources/translations/zh_cn/README.md @@ -2,7 +2,7 @@ [![Build](https://github.com/rrousselGit/flutter_hooks/workflows/Build/badge.svg)](https://github.com/rrousselGit/flutter_hooks/actions?query=workflow%3ABuild) [![codecov](https://codecov.io/gh/rrousselGit/flutter_hooks/branch/master/graph/badge.svg)](https://codecov.io/gh/rrousselGit/flutter_hooks) [![pub package](https://img.shields.io/pub/v/flutter_hooks.svg)](https://pub.dev/packages/flutter_hooks) [![pub package](https://img.shields.io/badge/Awesome-Flutter-blue.svg?longCache=true&style=flat-square)](https://github.com/Solido/awesome-flutter) -Discord +Discord

From 13bff1c5ede098b3e6d1921203c885c001362aa4 Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Tue, 19 Aug 2025 14:55:23 +0200 Subject: [PATCH 380/384] flutter_hooks : 0.21.3 -> 0.21.3+1 --- packages/flutter_hooks/CHANGELOG.md | 2 +- packages/flutter_hooks/pubspec.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/flutter_hooks/CHANGELOG.md b/packages/flutter_hooks/CHANGELOG.md index 74a48e57..bf33d63a 100644 --- a/packages/flutter_hooks/CHANGELOG.md +++ b/packages/flutter_hooks/CHANGELOG.md @@ -1,4 +1,4 @@ -## Unreleased 0.21.3+1 +## 0.21.3+1 - 2025-08-19 Updated Discord link diff --git a/packages/flutter_hooks/pubspec.yaml b/packages/flutter_hooks/pubspec.yaml index f42a2010..c2731f78 100644 --- a/packages/flutter_hooks/pubspec.yaml +++ b/packages/flutter_hooks/pubspec.yaml @@ -3,7 +3,7 @@ description: A flutter implementation of React hooks. It adds a new kind of widg homepage: https://github.com/rrousselGit/flutter_hooks repository: https://github.com/rrousselGit/flutter_hooks/tree/master/packages/flutter_hooks issue_tracker: https://github.com/rrousselGit/flutter_hooks/issues -version: 0.21.3 +version: 0.21.3+1 environment: sdk: ">=2.17.0 <3.0.0" From e8b1465171122ba533edcf66617af08af50da486 Mon Sep 17 00:00:00 2001 From: "cent.work" Date: Thu, 4 Sep 2025 16:32:32 +0800 Subject: [PATCH 381/384] =?UTF-8?q?=F0=9F=93=84=20docs:=20Synced=20newly?= =?UTF-8?q?=20added=20hooks=20to=20the=20Chinese=20documentation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../resources/translations/zh_cn/README.md | 108 ++++++++++-------- 1 file changed, 59 insertions(+), 49 deletions(-) diff --git a/packages/flutter_hooks/resources/translations/zh_cn/README.md b/packages/flutter_hooks/resources/translations/zh_cn/README.md index 9410be60..155e38f8 100644 --- a/packages/flutter_hooks/resources/translations/zh_cn/README.md +++ b/packages/flutter_hooks/resources/translations/zh_cn/README.md @@ -276,7 +276,7 @@ useC(); } ``` -## 已有的钩子 +## 已有的 Hook Flutter_Hooks 已经包含一些不同类别的可复用的钩子: @@ -284,68 +284,78 @@ Flutter_Hooks 已经包含一些不同类别的可复用的钩子: 与组件不同生命周期交互的低级钩子。 -| 名称 | 介绍 | -| -------------------------------------------------------------------------------------------------------- | ------------------------------------------- | -| [useEffect](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useEffect.html) | 对副作用很有用,可以选择取消它们 | -| [useState](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useState.html) | 创建并订阅一个变量 | -| [useMemoized](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useMemoized.html) | 缓存复杂对象的实例 | -| [useRef](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useRef.html) | 创建一个包含单个可变属性的对象 | -| [useCallback](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useCallback.html) | 缓存一个函数的实例 | -| [useContext](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useContext.html) | 包含构建中的 `HookWidget` 的 `BuildContext` | -| [useValueChanged](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useValueChanged.html) | 监听一个值并在其改变时触发回调 | +| 名称 | 描述 | +| -------------------------------------------------------------------------------------------------------- | -------------------------------------------- | +| [useEffect](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useEffect.html) | 用于处理副作用,并可选择性地进行清理 | +| [useState](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useState.html) | 创建一个变量并订阅它的变化 | +| [useMemoized](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useMemoized.html) | 缓存复杂对象的实例 | +| [useRef](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useRef.html) | 创建一个包含单个可变属性的对象 | +| [useCallback](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useCallback.html) | 缓存函数实例 | +| [useContext](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useContext.html) | 获取当前 `HookWidget` 的 `BuildContext` | +| [useValueChanged](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useValueChanged.html) | 监听某个值的变化,并在值发生变化时触发回调 | -### 绑定对象 +### 对象绑定(Object-binding) -这类钩子用以操作现有的 Flutter 及 Dart 对象。\ -它们负责创建、更新以及 dispose 对象。 +这类钩子用于操作现有的 Flutter 及 Dart 对象\ +它们负责创建、更新以及 dispose 对象 #### dart:async 相关 -| 名称 | 介绍 | +| 名称 | 描述 | | ---------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------- | -| [useStream](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useStream.html) | 订阅一个 `Stream`,并以 `AsyncSnapshot` 返回它目前的状态 | -| [useStreamController](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useStreamController.html) | 创建一个会自动 dispose 的 `StreamController` | -| [useOnStreamChange](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useOnStreamChange.html) | 订阅一个 `Stream`,注册处理函数,返回 `StreamSubscription` | -| [useFuture](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useFuture.html) | 订阅一个 `Future` 并以 `AsyncSnapshot` 返回它目前的状态 | +| [useStream](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useStream.html) | 订阅一个 `Stream` 并返回其当前状态(`AsyncSnapshot`) | +| [useStreamController](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useStreamController.html) | 创建一个 `StreamController`,会在不再使用时自动释放 | +| [useOnStreamChange](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useOnStreamChange.html) | 订阅 `Stream`,注册处理函数,并返回 `StreamSubscription` | +| [useFuture](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useFuture.html) | 订阅一个 `Future` 并返回其当前状态(`AsyncSnapshot`) | #### Animation 相关 -| 名称 | 介绍 | -| ------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------- | -| [useSingleTickerProvider](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useSingleTickerProvider.html) | 创建单一用途的 `TickerProvider` | -| [useAnimationController](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useAnimationController.html) | 创建一个会自动 dispose 的 `AnimationController` | -| [useAnimation](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useAnimation.html) | 订阅一个 `Animation` 并返回它的值 | +| 名称 | 描述 | +| ------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------ | +| [useSingleTickerProvider](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useSingleTickerProvider.html) | 创建单次使用的 `TickerProvider` | +| [useAnimationController](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useAnimationController.html) | 创建并会自动释放的 `AnimationController` | +| [useAnimation](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useAnimation.html) | 订阅一个 `Animation` 并返回其当前值 | -#### Listenable 相关 +#### Listenable 相关 Hook -| 名称 | 介绍 | -| -------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------- | -| [useListenable](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useListenable.html) | 订阅一个 `Listenable` 并在 listener 调用时将组件标脏 | -| [useListenableSelector](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useListenableSelector.html) | 和 `useListenable` 类似,但支持过滤 UI 重建 | -| [useValueNotifier](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useValueNotifier.html) | 创建一个会自动 dispose 的 `ValueNotifier` | -| [useValueListenable](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useValueListenable.html) | 订阅一个 `ValueListenable` 并返回它的值 | +| 名称 | 描述 | +| -------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------- | +| [useListenable](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useListenable.html) | 订阅一个 `Listenable`,当回调触发时标记组件需要重建 | +| [useListenableSelector](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useListenableSelector.html) | 类似于 `useListenable`,但允许过滤 UI 重建 | +| [useValueNotifier](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useValueNotifier.html) | 创建一个 `ValueNotifier`,并在不再使用时自动释放 | +| [useValueListenable](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useValueListenable.html) | 订阅一个 `ValueListenable` 并返回其值 | +| [useOnListenableChange](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useOnListenableChange.html) | 为 `Listenable` 添加回调,并在不再需要时自动移除 | #### 杂项 -一组无明确主题的钩子。 - -| 名称 | 介绍 | -| ------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------- | -| [useReducer](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useReducer.html) | 对于更复杂的状态,用以替代 `useState` | -| [usePrevious](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/usePrevious.html) | 返回调用 `usePrevious` 的上一个参数 | -| [useTextEditingController](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useTextEditingController-constant.html) | 创建一个 `TextEditingController` | -| [useFocusNode](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useFocusNode.html) | 创建一个 `FocusNode` | -| [useTabController](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useTabController.html) | 创建并自动 dispose 一个 `TabController` | -| [useScrollController](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useScrollController.html) | 创建并自动 dispose 一个 `ScrollController` | -| [usePageController](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/usePageController.html) | 创建并自动 dispose 一个 `PageController` | -| [useAppLifecycleState](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useAppLifecycleState.html) | 返回当前的 `AppLifecycleState`,并在改变时重建组件 | -| [useOnAppLifecycleStateChange](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useOnAppLifecycleStateChange.html) | 监听 `AppLifecycleState` 并在其改变时触发回调 | -| [useTransformationController](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useTransformationController.html) | 创建并自动 dispose 一个 `TransformationController` | -| [useIsMounted](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useIsMounted.html) | 对钩子而言和 `State.mounted` 一样 | -| [useAutomaticKeepAlive](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useAutomaticKeepAlive.html) | 对钩子而言和 `AutomaticKeepAlive` 一样 | -| [useOnPlatformBrightnessChange](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useOnPlatformBrightnessChange.html) | 监听平台 `Brightness` 并在其改变时触发回调 | -| [useWidgetStatesController](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useWidgetStatesController.html) | 创建并自动 dispose 一个 `WidgetStatesController` | -| [useExpansionTileController](https://api.flutter.dev/flutter/material/ExpansionTileController-class.html) | 创建一个 `ExpansionTileController` | +一组无明确主题的钩子 + +| 名称 | 描述 | +| -------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------- | +| [useReducer](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useReducer.html) | `useState` 的替代方案,适用于更复杂的状态管理 | +| [usePrevious](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/usePrevious.html) | 返回上一次调用 `usePrevious` 时的参数 | +| [useTextEditingController](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useTextEditingController-constant.html) | 创建一个 `TextEditingController` | +| [useFocusNode](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useFocusNode.html) | 创建一个 `FocusNode` | +| [useTabController](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useTabController.html) | 创建并自动释放一个 `TabController` | +| [useScrollController](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useScrollController.html) | 创建并自动释放一个 `ScrollController` | +| [usePageController](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/usePageController.html) | 创建并自动释放一个 `PageController` | +| [useFixedExtentScrollController](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useFixedExtentScrollController.html) | 创建并自动释放一个 `FixedExtentScrollController` | +| [useAppLifecycleState](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useAppLifecycleState.html) | 返回当前的 `AppLifecycleState`,并在其变化时触发组件重建 | +| [useOnAppLifecycleStateChange](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useOnAppLifecycleStateChange.html) | 监听 `AppLifecycleState` 的变化,并在变化时触发回调 | +| [useTransformationController](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useTransformationController.html) | 创建并自动释放一个 `TransformationController` | +| [useIsMounted](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useIsMounted.html) | Hook 版本的 `State.mounted` | +| [useAutomaticKeepAlive](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useAutomaticKeepAlive.html) | Hook 版本的 `AutomaticKeepAlive` 组件 | +| [useOnPlatformBrightnessChange](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useOnPlatformBrightnessChange.html) | 监听平台亮度(`Brightness`)变化,并在变化时触发回调 | +| [useSearchController](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useSearchController.html) | 创建并自动释放一个 `SearchController` | +| [useWidgetStatesController](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useWidgetStatesController.html) | 创建并自动释放一个 `WidgetStatesController` | +| [useExpansibleController](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useExpansibleController.html) | 创建一个 `ExpansibleController` | +| [useDebounced](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useDebounced.html) | 返回一个防抖后的值,在指定延时后触发更新 | +| [useDraggableScrollableController](https://api.flutter.dev/flutter/widgets/DraggableScrollableController-class.html) | 创建一个 `DraggableScrollableController` | +| [useCarouselController](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useCarouselController.html) | 创建并自动释放一个 `CarouselController` | +| [useTreeSliverController](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useTreeSliverController.html) | 创建一个 `TreeSliverController` | +| [useOverlayPortalController](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useOverlayPortalController.html) | 创建并管理一个 `OverlayPortalController`,用于控制覆盖层内容的可见性,会在不再需要时自动释放 | +| [useSnapshotController](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useSnapshotController.html) | 创建并管理一个 `SnapshotController` | +| [useCupertinoController](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useCupertinoController.html) | 创建并管理一个 `CupertinoController` | ## 贡献 From 746a8d136f53da5c3844445006541af18ba0fc1e Mon Sep 17 00:00:00 2001 From: Chandler <56182235+lm83680@users.noreply.github.com> Date: Thu, 4 Sep 2025 16:34:39 +0800 Subject: [PATCH 382/384] Update README.md --- packages/flutter_hooks/resources/translations/zh_cn/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/flutter_hooks/resources/translations/zh_cn/README.md b/packages/flutter_hooks/resources/translations/zh_cn/README.md index 155e38f8..a46698d5 100644 --- a/packages/flutter_hooks/resources/translations/zh_cn/README.md +++ b/packages/flutter_hooks/resources/translations/zh_cn/README.md @@ -316,7 +316,7 @@ Flutter_Hooks 已经包含一些不同类别的可复用的钩子: | [useAnimationController](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useAnimationController.html) | 创建并会自动释放的 `AnimationController` | | [useAnimation](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useAnimation.html) | 订阅一个 `Animation` 并返回其当前值 | -#### Listenable 相关 Hook +#### Listenable 相关 | 名称 | 描述 | | -------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------- | From b87b9b46ec5c14a683aaf803151748332703719a Mon Sep 17 00:00:00 2001 From: Vice Phenek Date: Sun, 30 Nov 2025 21:59:01 +0100 Subject: [PATCH 383/384] - useMultiTickerProvider --- packages/flutter_hooks/lib/src/animation.dart | 92 +++++++++++++ .../test/use_multi_ticker_provider_test.dart | 125 ++++++++++++++++++ 2 files changed, 217 insertions(+) create mode 100644 packages/flutter_hooks/test/use_multi_ticker_provider_test.dart diff --git a/packages/flutter_hooks/lib/src/animation.dart b/packages/flutter_hooks/lib/src/animation.dart index 86c0c8ce..08bcac7a 100644 --- a/packages/flutter_hooks/lib/src/animation.dart +++ b/packages/flutter_hooks/lib/src/animation.dart @@ -237,3 +237,95 @@ class _TickerProviderHookState @override bool get debugSkipValue => true; } + +/// Creates a [TickerProvider] that supports creating multiple [Ticker]s. +/// +/// See also: +/// * [SingleTickerProviderStateMixin] +TickerProvider useMultiTickerProvider({List? keys}) { + return use( + keys != null + ? _MultiTickerProviderHook(keys) + : const _MultiTickerProviderHook(), + ); +} + +class _MultiTickerProviderHook extends Hook { + const _MultiTickerProviderHook([List? keys]) : super(keys: keys); + + @override + _MultiTickerProviderHookState createState() => _MultiTickerProviderHookState(); +} + +class _MultiTickerProviderHookState + extends HookState + implements TickerProvider { + final Set _tickers = {}; + ValueListenable? _tickerModeNotifier; + + @override + Ticker createTicker(TickerCallback onTick) { + final ticker = Ticker(onTick, debugLabel: 'created by $context (multi)'); + _updateTickerModeNotifier(); + _updateTickers(); + _tickers.add(ticker); + return ticker; + } + + @override + void dispose() { + assert(() { + // Ensure there are no active tickers left. Controllers that own Tickers + // are responsible for disposing them — leaving an active ticker here is + // almost always a leak or misuse. + for (final t in _tickers) { + if (t.isActive) { + throw FlutterError( + 'useMultiTickerProvider created Ticker(s), but at the time ' + 'dispose() was called on the Hook, at least one of those Tickers ' + 'was still active. Tickers used by AnimationControllers should ' + 'be disposed by calling dispose() on the AnimationController ' + 'itself. Otherwise, the ticker will leak.\n'); + } + } + return true; + }(), ''); + + _tickerModeNotifier?.removeListener(_updateTickers); + _tickerModeNotifier = null; + _tickers.clear(); + super.dispose(); + } + + @override + TickerProvider build(BuildContext context) { + _updateTickerModeNotifier(); + _updateTickers(); + return this; + } + + void _updateTickers() { + if (_tickers.isNotEmpty) { + final muted = !(_tickerModeNotifier?.value ?? TickerMode.of(context)); + for (final t in _tickers) { + t.muted = muted; + } + } + } + + void _updateTickerModeNotifier() { + final newNotifier = TickerMode.getNotifier(context); + if (newNotifier == _tickerModeNotifier) { + return; + } + _tickerModeNotifier?.removeListener(_updateTickers); + newNotifier.addListener(_updateTickers); + _tickerModeNotifier = newNotifier; + } + + @override + String get debugLabel => 'useMultiTickerProvider'; + + @override + bool get debugSkipValue => true; +} diff --git a/packages/flutter_hooks/test/use_multi_ticker_provider_test.dart b/packages/flutter_hooks/test/use_multi_ticker_provider_test.dart new file mode 100644 index 00000000..88bef9ef --- /dev/null +++ b/packages/flutter_hooks/test/use_multi_ticker_provider_test.dart @@ -0,0 +1,125 @@ +import 'dart:async'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; + +import 'mock.dart'; + +void main() { + testWidgets('debugFillProperties', (tester) async { + await tester.pumpWidget( + HookBuilder(builder: (context) { + useMultiTickerProvider(); + return const SizedBox(); + }), + ); + + await tester.pump(); + + final element = tester.element(find.byType(HookBuilder)); + + expect( + element + .toDiagnosticsNode(style: DiagnosticsTreeStyle.offstage) + .toStringDeep(), + equalsIgnoringHashCodes( + 'HookBuilder\n' + ' │ useMultiTickerProvider\n' + ' └SizedBox(renderObject: RenderConstrainedBox#00000)\n', + ), + ); + }); + + testWidgets('useMultiTickerProvider basic', (tester) async { + late TickerProvider provider; + + await tester.pumpWidget(TickerMode( + enabled: true, + child: HookBuilder(builder: (context) { + provider = useMultiTickerProvider(); + return Container(); + }), + )); + + final animationControllerA = AnimationController( + vsync: provider, + duration: const Duration(seconds: 1), + ); + final animationControllerB = AnimationController( + vsync: provider, + duration: const Duration(seconds: 1), + ); + + unawaited(animationControllerA.forward()); + unawaited(animationControllerB.forward()); + + // With a multi provider, creating additional AnimationControllers is allowed. + expect( + () => AnimationController(vsync: provider, duration: const Duration(seconds: 1)), + returnsNormally, + ); + + animationControllerA.dispose(); + animationControllerB.dispose(); + + await tester.pumpWidget(const SizedBox()); + }); + + testWidgets('useMultiTickerProvider unused', (tester) async { + await tester.pumpWidget(HookBuilder(builder: (context) { + useMultiTickerProvider(); + return Container(); + })); + + await tester.pumpWidget(const SizedBox()); + }); + + testWidgets('useMultiTickerProvider still active', (tester) async { + late TickerProvider provider; + + await tester.pumpWidget(TickerMode( + enabled: true, + child: HookBuilder(builder: (context) { + provider = useMultiTickerProvider(); + return Container(); + }), + )); + + final animationController = AnimationController( + vsync: provider, + duration: const Duration(seconds: 1), + ); + + try { + // ignore: unawaited_futures + animationController.forward(); + + await tester.pumpWidget(const SizedBox()); + + expect(tester.takeException(), isFlutterError); + } finally { + animationController.dispose(); + } + }); + + testWidgets('useMultiTickerProvider pass down keys', (tester) async { + late TickerProvider provider; + List? keys; + + await tester.pumpWidget(HookBuilder(builder: (context) { + provider = useMultiTickerProvider(keys: keys); + return Container(); + })); + + final previousProvider = provider; + keys = []; + + await tester.pumpWidget(HookBuilder(builder: (context) { + provider = useMultiTickerProvider(keys: keys); + return Container(); + })); + + expect(previousProvider, isNot(provider)); + }); +} From c3097b0822e7ab5cd53981219f69be1784304501 Mon Sep 17 00:00:00 2001 From: Vice Phenek Date: Sun, 30 Nov 2025 22:20:13 +0100 Subject: [PATCH 384/384] - readme for useMultiTickerProvider --- README.md | 1 + packages/flutter_hooks/resources/translations/ja_jp/README.md | 1 + packages/flutter_hooks/resources/translations/ko_kr/README.md | 1 + packages/flutter_hooks/resources/translations/pt_br/README.md | 1 + packages/flutter_hooks/resources/translations/zh_cn/README.md | 1 + 5 files changed, 5 insertions(+) diff --git a/README.md b/README.md index ee67f958..24d2774a 100644 --- a/README.md +++ b/README.md @@ -320,6 +320,7 @@ They will take care of creating/updating/disposing an object. | Name | Description | | ------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------- | | [useSingleTickerProvider](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useSingleTickerProvider.html) | Creates a single usage `TickerProvider`. | +| [useMultiTickerProvider](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useMultiTickerProvider.html) | Creates a `TickerProvider` that supports creating multiple `Ticker`s. | | [useAnimationController](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useAnimationController.html) | Creates an `AnimationController` which will be automatically disposed. | | [useAnimation](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useAnimation.html) | Subscribes to an `Animation` and returns its value. | diff --git a/packages/flutter_hooks/resources/translations/ja_jp/README.md b/packages/flutter_hooks/resources/translations/ja_jp/README.md index 98e015ef..f05f58c0 100644 --- a/packages/flutter_hooks/resources/translations/ja_jp/README.md +++ b/packages/flutter_hooks/resources/translations/ja_jp/README.md @@ -304,6 +304,7 @@ Flutter_Hooksには、再利用可能なフックのリストが既に含まれ | 名前 | 説明 | | ------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------- | | [useSingleTickerProvider](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useSingleTickerProvider.html) | 単一使用の`TickerProvider`を作成します。 | +| [useMultiTickerProvider](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useMultiTickerProvider.html) | 複数の`Ticker`を作成できる`TickerProvider`を作成します。 | | [useAnimationController](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useAnimationController.html) | 自動的に破棄される`AnimationController`を作成します。 | | [useAnimation](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useAnimation.html) | `Animation`を購読し、その値を返します。 | diff --git a/packages/flutter_hooks/resources/translations/ko_kr/README.md b/packages/flutter_hooks/resources/translations/ko_kr/README.md index 112dd7e2..4137b120 100644 --- a/packages/flutter_hooks/resources/translations/ko_kr/README.md +++ b/packages/flutter_hooks/resources/translations/ko_kr/README.md @@ -302,6 +302,7 @@ Flutter_Hooks 는 이미 재사용 가능한 훅 목록을 제공합니다. 이 | Name | Description | | --------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------- | | [useSingleTickerProvider](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useSingleTickerProvider.html) | `TickerProvider`를 생성합니다. | +| [useMultiTickerProvider](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useMultiTickerProvider.html) | 여러 `Ticker`를 생성할 수 있는 `TickerProvider`를 생성합니다. | | [useAnimationController](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useAnimationController.html) | 자동으로 dispose 되는 `AnimationController`를 생성합니다. | | [useAnimation](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useAnimation.html) | `Animation` 를 구독합니다. 해당 객체의 value를 반환합니다. | diff --git a/packages/flutter_hooks/resources/translations/pt_br/README.md b/packages/flutter_hooks/resources/translations/pt_br/README.md index a804cdee..6d169a4c 100644 --- a/packages/flutter_hooks/resources/translations/pt_br/README.md +++ b/packages/flutter_hooks/resources/translations/pt_br/README.md @@ -322,6 +322,7 @@ Eles serão responsáveis por criar/atualizar/descartar o objeto. | nome | descrição | | --------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------- | | [useSingleTickerProvider](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useSingleTickerProvider.html) | Cria um único `TickerProvider`. | +| [useMultiTickerProvider](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useMultiTickerProvider.html) | Cria um `TickerProvider` que suporta criar múltiplos `Ticker`s. | | [useAnimationController](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useAnimationController.html) | Cria um `AnimationController` automaticamente descartado. | | [useAnimation](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useAnimation.html) | Inscreve um uma `Animation` e retorna seu valor. | diff --git a/packages/flutter_hooks/resources/translations/zh_cn/README.md b/packages/flutter_hooks/resources/translations/zh_cn/README.md index a46698d5..2e2d15a6 100644 --- a/packages/flutter_hooks/resources/translations/zh_cn/README.md +++ b/packages/flutter_hooks/resources/translations/zh_cn/README.md @@ -313,6 +313,7 @@ Flutter_Hooks 已经包含一些不同类别的可复用的钩子: | 名称 | 描述 | | ------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------ | | [useSingleTickerProvider](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useSingleTickerProvider.html) | 创建单次使用的 `TickerProvider` | +| [useMultiTickerProvider](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useMultiTickerProvider.html) | 创建支持多个 `Ticker` 的 `TickerProvider` | | [useAnimationController](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useAnimationController.html) | 创建并会自动释放的 `AnimationController` | | [useAnimation](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useAnimation.html) | 订阅一个 `Animation` 并返回其当前值 |