├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── config.yml │ ├── example_request.md │ └── feature_request.md ├── dependabot.yaml └── workflows │ ├── build.yml │ └── project.yml ├── LICENSE ├── README.md └── packages └── flutter_hooks ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── all_lint_rules.yaml ├── analysis_options.yaml ├── example ├── .gitignore ├── .metadata ├── README.md ├── analysis_options.yaml ├── lib │ ├── custom_hook_function.dart │ ├── main.dart │ ├── star_wars │ │ ├── README.md │ │ ├── models.dart │ │ ├── models.g.dart │ │ ├── planet_screen.dart │ │ ├── redux.dart │ │ ├── redux.g.dart │ │ └── star_wars_api.dart │ ├── use_effect.dart │ ├── use_reducer.dart │ ├── use_state.dart │ └── use_stream.dart └── pubspec.yaml ├── flutter-hook.svg ├── lib ├── flutter_hooks.dart └── src │ ├── animation.dart │ ├── async.dart │ ├── carousel_controller.dart │ ├── debounced.dart │ ├── draggable_scrollable_controller.dart │ ├── expansion_tile_controller.dart │ ├── fixed_extent_scroll_controller.dart │ ├── focus_node.dart │ ├── focus_scope_node.dart │ ├── framework.dart │ ├── hooks.dart │ ├── keep_alive.dart │ ├── listenable.dart │ ├── listenable_selector.dart │ ├── misc.dart │ ├── page_controller.dart │ ├── platform_brightness.dart │ ├── primitives.dart │ ├── scroll_controller.dart │ ├── search_controller.dart │ ├── tab_controller.dart │ ├── text_controller.dart │ ├── transformation_controller.dart │ ├── tree_sliver_controller.dart │ ├── widget_states_controller.dart │ └── widgets_binding_observer.dart ├── pubspec.yaml ├── resources └── translations │ ├── ja_jp │ └── README.md │ ├── ko_kr │ └── README.md │ ├── pt_br │ └── README.md │ └── zh_cn │ └── README.md └── test ├── carousel_controller_test.dart ├── hook_builder_test.dart ├── hook_widget_test.dart ├── memoized_test.dart ├── mock.dart ├── pre_build_abort_test.dart ├── use_animation_controller_test.dart ├── use_animation_test.dart ├── use_app_lifecycle_state_test.dart ├── use_automatic_keep_alive_test.dart ├── use_callback_test.dart ├── use_context_test.dart ├── use_debounce_test.dart ├── use_draggable_scrollable_controller_test.dart ├── use_effect_test.dart ├── use_expansible_controller_test.dart ├── use_fixed_extent_scroll_controller_test.dart ├── use_focus_node_test.dart ├── use_focus_scope_node_test.dart ├── use_future_test.dart ├── use_listenable_selector_test.dart ├── use_listenable_test.dart ├── use_material_states_controller_test.dart ├── use_on_listenable_change_test.dart ├── use_on_stream_change_test.dart ├── use_overlay_portal_controller_test.dart ├── use_page_controller_test.dart ├── use_platform_brightness_test.dart ├── use_previous_test.dart ├── use_reassemble_test.dart ├── use_reducer_test.dart ├── use_scroll_controller_test.dart ├── use_search_controller_test.dart ├── use_state_test.dart ├── use_stream_controller_test.dart ├── use_stream_test.dart ├── use_tab_controller_test.dart ├── use_text_editing_controller_test.dart ├── use_ticker_provider_test.dart ├── use_transformation_controller_test.dart ├── use_tree_sliver_controller_test.dart ├── use_value_changed_test.dart ├── use_value_listenable_test.dart └── use_value_notifier_test.dart /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: rrousselGit 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: There is a problem in how provider behaves 4 | title: "" 5 | labels: bug, needs triage 6 | assignees: 7 | - rrousselGit 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | 15 | 16 | 17 | **Expected behavior** 18 | A clear and concise description of what you expected to happen. 19 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: I have a problem and I need help 4 | url: https://github.com/rrousselGit/flutter_hooks/discussions 5 | about: Please ask and answer questions here. 6 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/example_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Documentation improvement request 3 | about: >- 4 | Suggest a new example/documentation or ask for clarification about an 5 | existing one. 6 | title: "" 7 | labels: documentation, needs triage 8 | assignees: 9 | - rrousselGit 10 | --- 11 | 12 | **Describe what scenario you think is uncovered by the existing examples/articles** 13 | A clear and concise description of the problem that you want explained. 14 | 15 | **Describe why existing examples/articles do not cover this case** 16 | Explain which examples/articles you have seen before making this request, and 17 | why they did not help you with your problem. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the documentation request here. 21 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: "" 5 | labels: enhancement, needs triage 6 | assignees: 7 | - rrousselGit 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/dependabot.yaml: -------------------------------------------------------------------------------- 1 | version: 2 2 | enable-beta-ecosystems: true 3 | updates: 4 | - package-ecosystem: "pub" 5 | directory: "/" 6 | schedule: 7 | interval: "weekly" -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | push: 5 | pull_request: 6 | schedule: 7 | # runs the CI everyday at 10AM 8 | - cron: "0 10 * * *" 9 | 10 | jobs: 11 | flutter: 12 | runs-on: ubuntu-latest 13 | 14 | strategy: 15 | matrix: 16 | package: 17 | - flutter_hooks 18 | channel: 19 | - master 20 | - stable 21 | fail-fast: false 22 | 23 | steps: 24 | - uses: actions/checkout@v2 25 | - uses: subosito/flutter-action@v1 26 | with: 27 | channel: ${{ matrix.channel }} 28 | 29 | - name: Install dependencies 30 | run: flutter pub get 31 | working-directory: packages/${{ matrix.package }} 32 | 33 | - name: Check format 34 | run: dart format --set-exit-if-changed . 35 | if: matrix.channel == 'stable' 36 | working-directory: packages/${{ matrix.package }} 37 | 38 | - name: Analyze 39 | run: dart analyze . 40 | if: matrix.channel == 'stable' 41 | working-directory: packages/${{ matrix.package }} 42 | 43 | - name: Run tests 44 | run: flutter test --coverage 45 | working-directory: packages/${{ matrix.package }} 46 | 47 | - name: Upload coverage to codecov 48 | run: curl -s https://codecov.io/bash | bash 49 | if: matrix.channel == 'stable' 50 | working-directory: packages/${{ matrix.package }} 51 | -------------------------------------------------------------------------------- /.github/workflows/project.yml: -------------------------------------------------------------------------------- 1 | name: Add new issues to project 2 | 3 | on: 4 | issues: 5 | types: 6 | - opened 7 | - reopened 8 | 9 | jobs: 10 | add-to-project: 11 | name: Add issue to project 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/add-to-project@v0.5.0 15 | with: 16 | project-url: https://github.com/users/rrousselGit/projects/8 17 | github-token: ${{ secrets.ADD_TO_PROJECT_PAT }} 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Remi Rousselet 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /packages/flutter_hooks/.gitignore: -------------------------------------------------------------------------------- 1 | # Flutter/Dart/Pub related 2 | **/doc/api/ 3 | .dart_tool/ 4 | .flutter-plugins 5 | .packages 6 | .pub-cache/ 7 | .pub/ 8 | build/ 9 | android/ 10 | ios/ 11 | /coverage 12 | pubspec.lock 13 | .vscode/ 14 | .fvm 15 | .idea/ 16 | -------------------------------------------------------------------------------- /packages/flutter_hooks/LICENSE: -------------------------------------------------------------------------------- 1 | ../../LICENSE -------------------------------------------------------------------------------- /packages/flutter_hooks/README.md: -------------------------------------------------------------------------------- 1 | ../../README.md -------------------------------------------------------------------------------- /packages/flutter_hooks/all_lint_rules.yaml: -------------------------------------------------------------------------------- 1 | linter: 2 | rules: 3 | - always_declare_return_types 4 | - always_put_control_body_on_new_line 5 | - always_put_required_named_parameters_first 6 | - always_require_non_null_named_parameters 7 | - always_specify_types 8 | - always_use_package_imports 9 | - annotate_overrides 10 | - avoid_annotating_with_dynamic 11 | - avoid_bool_literals_in_conditional_expressions 12 | - avoid_catches_without_on_clauses 13 | - avoid_catching_errors 14 | - avoid_classes_with_only_static_members 15 | - avoid_double_and_int_checks 16 | - avoid_dynamic_calls 17 | - avoid_empty_else 18 | - avoid_equals_and_hash_code_on_mutable_classes 19 | - avoid_escaping_inner_quotes 20 | - avoid_field_initializers_in_const_classes 21 | - avoid_function_literals_in_foreach_calls 22 | - avoid_implementing_value_types 23 | - avoid_init_to_null 24 | - avoid_js_rounded_ints 25 | - avoid_null_checks_in_equality_operators 26 | - avoid_positional_boolean_parameters 27 | - avoid_print 28 | - avoid_private_typedef_functions 29 | - avoid_redundant_argument_values 30 | - avoid_relative_lib_imports 31 | - avoid_renaming_method_parameters 32 | - avoid_return_types_on_setters 33 | - avoid_returning_null 34 | - avoid_returning_null_for_future 35 | - avoid_returning_null_for_void 36 | - avoid_returning_this 37 | - avoid_setters_without_getters 38 | - avoid_shadowing_type_parameters 39 | - avoid_single_cascade_in_expression_statements 40 | - avoid_slow_async_io 41 | - avoid_type_to_string 42 | - avoid_types_as_parameter_names 43 | - avoid_types_on_closure_parameters 44 | - avoid_unnecessary_containers 45 | - avoid_unused_constructor_parameters 46 | - avoid_void_async 47 | - avoid_web_libraries_in_flutter 48 | - await_only_futures 49 | - camel_case_extensions 50 | - camel_case_types 51 | - cancel_subscriptions 52 | - cascade_invocations 53 | - cast_nullable_to_non_nullable 54 | - close_sinks 55 | - comment_references 56 | - constant_identifier_names 57 | - control_flow_in_finally 58 | - curly_braces_in_flow_control_structures 59 | - diagnostic_describe_all_properties 60 | - directives_ordering 61 | - do_not_use_environment 62 | - empty_catches 63 | - empty_constructor_bodies 64 | - empty_statements 65 | - exhaustive_cases 66 | - file_names 67 | - flutter_style_todos 68 | - hash_and_equals 69 | - implementation_imports 70 | - invariant_booleans 71 | - iterable_contains_unrelated_type 72 | - join_return_with_assignment 73 | - leading_newlines_in_multiline_strings 74 | - library_names 75 | - library_prefixes 76 | - lines_longer_than_80_chars 77 | - list_remove_unrelated_type 78 | - literal_only_boolean_expressions 79 | - missing_whitespace_between_adjacent_strings 80 | - no_adjacent_strings_in_list 81 | - no_default_cases 82 | - no_duplicate_case_values 83 | - no_logic_in_create_state 84 | - no_runtimeType_toString 85 | - non_constant_identifier_names 86 | - null_check_on_nullable_type_parameter 87 | - null_closures 88 | - omit_local_variable_types 89 | - one_member_abstracts 90 | - only_throw_errors 91 | - overridden_fields 92 | - package_api_docs 93 | - package_names 94 | - package_prefixed_library_names 95 | - parameter_assignments 96 | - prefer_adjacent_string_concatenation 97 | - prefer_asserts_in_initializer_lists 98 | - prefer_asserts_with_message 99 | - prefer_collection_literals 100 | - prefer_conditional_assignment 101 | - prefer_const_constructors 102 | - prefer_const_constructors_in_immutables 103 | - prefer_const_declarations 104 | - prefer_const_literals_to_create_immutables 105 | - prefer_constructors_over_static_methods 106 | - prefer_contains 107 | - prefer_double_quotes 108 | - prefer_equal_for_default_values 109 | - prefer_expression_function_bodies 110 | - prefer_final_fields 111 | - prefer_final_in_for_each 112 | - prefer_final_locals 113 | - prefer_for_elements_to_map_fromIterable 114 | - prefer_foreach 115 | - prefer_function_declarations_over_variables 116 | - prefer_generic_function_type_aliases 117 | - prefer_if_elements_to_conditional_expressions 118 | - prefer_if_null_operators 119 | - prefer_initializing_formals 120 | - prefer_inlined_adds 121 | - prefer_int_literals 122 | - prefer_interpolation_to_compose_strings 123 | - prefer_is_empty 124 | - prefer_is_not_empty 125 | - prefer_is_not_operator 126 | - prefer_iterable_whereType 127 | - prefer_mixin 128 | - prefer_null_aware_operators 129 | - prefer_relative_imports 130 | - prefer_single_quotes 131 | - prefer_spread_collections 132 | - prefer_typing_uninitialized_variables 133 | - prefer_void_to_null 134 | - provide_deprecation_message 135 | - public_member_api_docs 136 | - recursive_getters 137 | - sized_box_for_whitespace 138 | - slash_for_doc_comments 139 | - sort_child_properties_last 140 | - sort_constructors_first 141 | - sort_pub_dependencies 142 | - sort_unnamed_constructors_first 143 | - test_types_in_equals 144 | - throw_in_finally 145 | - tighten_type_of_initializing_formals 146 | - type_annotate_public_apis 147 | - type_init_formals 148 | - unawaited_futures 149 | - unnecessary_await_in_return 150 | - unnecessary_brace_in_string_interps 151 | - unnecessary_const 152 | - unnecessary_final 153 | - unnecessary_getters_setters 154 | - unnecessary_lambdas 155 | - unnecessary_new 156 | - unnecessary_null_aware_assignments 157 | - unnecessary_null_checks 158 | - unnecessary_null_in_if_null_operators 159 | - unnecessary_nullable_for_final_variable_declarations 160 | - unnecessary_overrides 161 | - unnecessary_parenthesis 162 | - unnecessary_raw_strings 163 | - unnecessary_statements 164 | - unnecessary_string_escapes 165 | - unnecessary_string_interpolations 166 | - unnecessary_this 167 | - unrelated_type_equality_checks 168 | - unsafe_html 169 | - use_full_hex_values_for_flutter_colors 170 | - use_function_type_syntax_for_parameters 171 | - use_is_even_rather_than_modulo 172 | - use_key_in_widget_constructors 173 | - use_late_for_private_fields_and_variables 174 | - use_raw_strings 175 | - use_rethrow_when_possible 176 | - use_setters_to_change_properties 177 | - use_string_buffers 178 | - use_to_and_as_if_applicable 179 | - valid_regexps 180 | - void_checks -------------------------------------------------------------------------------- /packages/flutter_hooks/analysis_options.yaml: -------------------------------------------------------------------------------- 1 | include: all_lint_rules.yaml 2 | analyzer: 3 | exclude: 4 | - "**/*.g.dart" 5 | - "**/*.freezed.dart" 6 | language: 7 | strict-casts: true 8 | strict-inference: true 9 | strict-raw-types: true 10 | errors: 11 | # Otherwise cause the import of all_lint_rules to warn because of some rules conflicts. 12 | # We explicitly enabled even conflicting rules and are fixing the conflict 13 | # in this file 14 | included_file_warning: ignore 15 | 16 | linter: 17 | rules: 18 | # Uninitianalized late variables are dangerous 19 | use_late_for_private_fields_and_variables: false 20 | 21 | # Personal preference. I don't find it more readable 22 | cascade_invocations: false 23 | 24 | # Conflicts with `prefer_single_quotes` 25 | # Single quotes are easier to type and don't compromise on readability. 26 | prefer_double_quotes: false 27 | 28 | # Conflicts with `omit_local_variable_types` and other rules. 29 | # As per Dart guidelines, we want to avoid unnecessary types to make the code 30 | # more readable. 31 | # See https://dart.dev/guides/language/effective-dart/design#avoid-type-annotating-initialized-local-variables 32 | always_specify_types: false 33 | 34 | # Incompatible with `prefer_final_locals` 35 | # Having immutable local variables makes larger functions more predictable 36 | # so we will use `prefer_final_locals` instead. 37 | unnecessary_final: false 38 | 39 | # Not quite suitable for Flutter, which may have a `build` method with a single 40 | # return, but that return is still complex enough that a "body" is worth it. 41 | prefer_expression_function_bodies: false 42 | 43 | # Conflicts with the convention used by flutter, which puts `Key key` 44 | # and `@required Widget child` last. 45 | always_put_required_named_parameters_first: false 46 | 47 | # This project doesn't use Flutter-style todos 48 | flutter_style_todos: false 49 | 50 | # There are situations where we voluntarily want to catch everything, 51 | # especially as a library. 52 | avoid_catches_without_on_clauses: false 53 | 54 | # Boring as it sometimes force a line of 81 characters to be split in two. 55 | # As long as we try to respect that 80 characters limit, going slightly 56 | # above is fine. 57 | lines_longer_than_80_chars: false 58 | 59 | # Conflicts with disabling `implicit-dynamic` 60 | avoid_annotating_with_dynamic: false 61 | 62 | # conflicts with `prefer_relative_imports` 63 | always_use_package_imports: false 64 | 65 | # Disabled for now until we have NNBD as it otherwise conflicts with `missing_return` 66 | no_default_cases: false 67 | 68 | # Too verbose 69 | diagnostic_describe_all_properties: false 70 | -------------------------------------------------------------------------------- /packages/flutter_hooks/example/.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.lock 4 | *.log 5 | *.pyc 6 | *.swp 7 | .DS_Store 8 | .atom/ 9 | .buildlog/ 10 | .history 11 | .svn/ 12 | 13 | # IntelliJ related 14 | *.iml 15 | *.ipr 16 | *.iws 17 | .idea/ 18 | 19 | # Visual Studio Code related 20 | .vscode/ 21 | 22 | # Flutter/Dart/Pub related 23 | **/doc/api/ 24 | .dart_tool/ 25 | .flutter-plugins 26 | .flutter-plugins-dependencies 27 | .packages 28 | .pub-cache/ 29 | .pub/ 30 | build/ 31 | 32 | # Android related 33 | **/android/**/gradle-wrapper.jar 34 | **/android/.gradle 35 | **/android/captures/ 36 | **/android/gradlew 37 | **/android/gradlew.bat 38 | **/android/local.properties 39 | **/android/**/GeneratedPluginRegistrant.java 40 | 41 | # iOS/XCode related 42 | **/ios/**/*.mode1v3 43 | **/ios/**/*.mode2v3 44 | **/ios/**/*.moved-aside 45 | **/ios/**/*.pbxuser 46 | **/ios/**/*.perspectivev3 47 | **/ios/**/*sync/ 48 | **/ios/**/.sconsign.dblite 49 | **/ios/**/.tags* 50 | **/ios/**/.vagrant/ 51 | **/ios/**/DerivedData/ 52 | **/ios/**/Icon? 53 | **/ios/**/Pods/ 54 | **/ios/**/.symlinks/ 55 | **/ios/**/profile 56 | **/ios/**/xcuserdata 57 | **/ios/.generated/ 58 | **/ios/Flutter/App.framework 59 | **/ios/Flutter/Flutter.framework 60 | **/ios/Flutter/Generated.xcconfig 61 | **/ios/Flutter/app.flx 62 | **/ios/Flutter/app.zip 63 | **/ios/Flutter/flutter_assets/ 64 | **/ios/ServiceDefinitions.json 65 | **/ios/Runner/GeneratedPluginRegistrant.* 66 | 67 | # Exceptions to above rules. 68 | !**/ios/**/default.mode1v3 69 | !**/ios/**/default.mode2v3 70 | !**/ios/**/default.pbxuser 71 | !**/ios/**/default.perspectivev3 72 | !/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages 73 | 74 | generated_plugin_registrant.dart 75 | /test/ 76 | -------------------------------------------------------------------------------- /packages/flutter_hooks/example/.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled and should not be manually edited. 5 | 6 | version: 7 | revision: 5391447fae6209bb21a89e6a5a6583cac1af9b4b 8 | channel: stable 9 | 10 | project_type: app 11 | -------------------------------------------------------------------------------- /packages/flutter_hooks/example/README.md: -------------------------------------------------------------------------------- 1 | # Flutter Hooks Gallery 2 | 3 | A series of examples demonstrating how to use Flutter Hooks! It teaches how to 4 | use the Widgets and hooks that are provided by this library, as well examples 5 | demonstrating how to write custom hooks. 6 | 7 | ## Run the app 8 | 9 | 1. Open a terminal 10 | 2. Navigate to this `example` directory 11 | 3. Run `flutter create .` 12 | 4. Run `flutter run` from your Terminal, or launch the project from your IDE! -------------------------------------------------------------------------------- /packages/flutter_hooks/example/analysis_options.yaml: -------------------------------------------------------------------------------- 1 | include: ../analysis_options.yaml 2 | analyzer: 3 | language: 4 | strict-casts: true 5 | strict-inference: true 6 | strict-raw-types: true 7 | errors: 8 | todo: error 9 | include_file_not_found: ignore 10 | linter: 11 | rules: 12 | public_member_api_docs: false 13 | avoid_print: false 14 | use_key_in_widget_constructors: false 15 | -------------------------------------------------------------------------------- /packages/flutter_hooks/example/lib/custom_hook_function.dart: -------------------------------------------------------------------------------- 1 | // ignore_for_file: omit_local_variable_types 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter_hooks/flutter_hooks.dart'; 4 | 5 | /// This example demonstrates how to write a hook function that enhances the 6 | /// useState hook with logging functionality. 7 | class CustomHookFunctionExample extends HookWidget { 8 | @override 9 | Widget build(BuildContext context) { 10 | // Next, invoke the custom `useLoggedState` hook with a default value to 11 | // create a `counter` variable that contains a `value`. Whenever the value 12 | // is changed, this Widget will be rebuilt and the result will be logged! 13 | final counter = useLoggedState(0); 14 | 15 | return Scaffold( 16 | appBar: AppBar( 17 | title: const Text('Custom Hook: Function'), 18 | ), 19 | body: Center( 20 | // Read the current value from the counter 21 | child: Text('Button tapped ${counter.value} times'), 22 | ), 23 | floatingActionButton: FloatingActionButton( 24 | // When the button is pressed, update the value of the counter! This 25 | // will trigger a rebuild as well as printing the latest value to the 26 | // console! 27 | onPressed: () => counter.value++, 28 | child: const Icon(Icons.add), 29 | ), 30 | ); 31 | } 32 | } 33 | 34 | /// A custom hook that wraps the useState hook to add logging. Hooks can be 35 | /// composed -- meaning you can use hooks within hooks! 36 | ValueNotifier useLoggedState(T initialData) { 37 | // First, call the useState hook. It will create a ValueNotifier for you that 38 | // rebuilds the Widget whenever the value changes. 39 | final result = useState(initialData); 40 | 41 | // Next, call the useValueChanged hook to print the state whenever it changes 42 | useValueChanged(result.value, (_, __) { 43 | print(result.value); 44 | }); 45 | 46 | return result; 47 | } 48 | -------------------------------------------------------------------------------- /packages/flutter_hooks/example/lib/main.dart: -------------------------------------------------------------------------------- 1 | // ignore_for_file: omit_local_variable_types 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter_hooks/flutter_hooks.dart'; 4 | 5 | import 'star_wars/planet_screen.dart'; 6 | import 'use_effect.dart'; 7 | import 'use_state.dart'; 8 | import 'use_stream.dart'; 9 | 10 | void main() => runApp(HooksGalleryApp()); 11 | 12 | /// An App that demonstrates how to use hooks. It includes examples that cover 13 | /// the hooks provided by this library as well as examples that demonstrate 14 | /// how to write custom hooks. 15 | class HooksGalleryApp extends HookWidget { 16 | @override 17 | Widget build(BuildContext context) { 18 | useAnimationController(duration: const Duration(seconds: 2)); 19 | return MaterialApp( 20 | title: 'Flutter Hooks Gallery', 21 | home: Scaffold( 22 | appBar: AppBar( 23 | title: const Text('Flutter Hooks Gallery'), 24 | ), 25 | body: ListView(children: [ 26 | _GalleryItem( 27 | title: 'useState', 28 | builder: (context) => UseStateExample(), 29 | ), 30 | _GalleryItem( 31 | title: 'useMemoize + useStream', 32 | builder: (context) => UseStreamExample(), 33 | ), 34 | _GalleryItem( 35 | title: 'Custom Hook Function', 36 | builder: (context) => CustomHookExample(), 37 | ), 38 | _GalleryItem( 39 | title: 'Star Wars Planets', 40 | builder: (context) => PlanetScreen(), 41 | ) 42 | ]), 43 | ), 44 | ); 45 | } 46 | } 47 | 48 | class _GalleryItem extends StatelessWidget { 49 | const _GalleryItem({ 50 | required this.title, 51 | required this.builder, 52 | }); 53 | 54 | final String title; 55 | final WidgetBuilder builder; 56 | 57 | @override 58 | Widget build(BuildContext context) { 59 | return ListTile( 60 | title: Text(title), 61 | onTap: () { 62 | Navigator.push( 63 | context, 64 | MaterialPageRoute( 65 | builder: builder, 66 | ), 67 | ); 68 | }, 69 | ); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /packages/flutter_hooks/example/lib/star_wars/README.md: -------------------------------------------------------------------------------- 1 | # Star wars API 2 | 3 | This folder is specifically for handling the Star wars API. This is one API endpoint: https://swapi.co/api/planets 4 | 5 | To understand the generated .g.dart code see https://flutter.dev/docs/development/data-and-backend/json#serializing-json-using-code-generation-libraries 6 | -------------------------------------------------------------------------------- /packages/flutter_hooks/example/lib/star_wars/models.dart: -------------------------------------------------------------------------------- 1 | // ignore_for_file: public_member_api_docs 2 | 3 | import 'package:built_collection/built_collection.dart'; 4 | import 'package:built_value/built_value.dart'; 5 | import 'package:built_value/serializer.dart'; 6 | import 'package:built_value/standard_json_plugin.dart'; 7 | import 'package:meta/meta.dart'; 8 | 9 | part 'models.g.dart'; 10 | 11 | /// json serializer to build models 12 | @SerializersFor([ 13 | PlanetPageModel, 14 | PlanetModel, 15 | ]) 16 | final Serializers serializers = 17 | (_$serializers.toBuilder()..addPlugin(StandardJsonPlugin())).build(); 18 | 19 | @immutable 20 | abstract class PlanetPageModel 21 | implements Built { 22 | factory PlanetPageModel([ 23 | void Function(PlanetPageModelBuilder) updates, 24 | ]) = _$PlanetPageModel; 25 | 26 | const PlanetPageModel._(); 27 | 28 | static Serializer get serializer => 29 | _$planetPageModelSerializer; 30 | 31 | String? get next; 32 | 33 | String? get previous; 34 | 35 | BuiltList get results; 36 | } 37 | 38 | @immutable 39 | abstract class PlanetModel implements Built { 40 | factory PlanetModel([ 41 | void Function(PlanetModelBuilder) updates, 42 | ]) = _$PlanetModel; 43 | 44 | const PlanetModel._(); 45 | 46 | static Serializer get serializer => _$planetModelSerializer; 47 | 48 | String get name; 49 | } 50 | -------------------------------------------------------------------------------- /packages/flutter_hooks/example/lib/star_wars/planet_screen.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_hooks/flutter_hooks.dart'; 3 | import 'package:provider/provider.dart'; 4 | 5 | import 'redux.dart'; 6 | import 'star_wars_api.dart'; 7 | 8 | /// This handler will take care of async api interactions 9 | /// and updating the store afterwards. 10 | class _PlanetHandler { 11 | _PlanetHandler(this._store, this._starWarsApi); 12 | 13 | final Store _store; 14 | final StarWarsApi _starWarsApi; 15 | 16 | /// This will load all planets and will dispatch all necessary actions 17 | /// on the redux store. 18 | Future fetchAndDispatch([String? url]) async { 19 | _store.dispatch(FetchPlanetPageActionStart()); 20 | try { 21 | final page = await _starWarsApi.getPlanets(url); 22 | _store.dispatch(FetchPlanetPageActionSuccess(page)); 23 | } catch (e, stack) { 24 | print('errpr $e $stack'); 25 | _store.dispatch(FetchPlanetPageActionError('Error loading Planets')); 26 | } 27 | } 28 | } 29 | 30 | /// This example will load, show and let you navigate through all star wars 31 | /// planets. 32 | /// 33 | /// It will demonstrate on how to use [Provider] and [useReducer] 34 | class PlanetScreen extends HookWidget { 35 | @override 36 | Widget build(BuildContext context) { 37 | final api = useMemoized(() => StarWarsApi()); 38 | 39 | final store = useReducer( 40 | reducer, 41 | initialState: AppState(), 42 | initialAction: null, 43 | ); 44 | 45 | final planetHandler = useMemoized( 46 | () { 47 | /// Create planet handler and load the first page. 48 | /// The first page will only be loaded once, after the handler was created 49 | return _PlanetHandler(store, api)..fetchAndDispatch(); 50 | }, 51 | [store, api], 52 | ); 53 | 54 | return MultiProvider( 55 | providers: [ 56 | Provider.value(value: planetHandler), 57 | Provider.value(value: store.state), 58 | ], 59 | child: Scaffold( 60 | appBar: AppBar( 61 | title: const Text( 62 | 'Star Wars Planets', 63 | ), 64 | ), 65 | body: const _PlanetScreenBody(), 66 | ), 67 | ); 68 | } 69 | } 70 | 71 | class _PlanetScreenBody extends HookWidget { 72 | const _PlanetScreenBody(); 73 | 74 | @override 75 | Widget build(BuildContext context) { 76 | final state = Provider.of(context); 77 | 78 | if (state.isFetchingPlanets) { 79 | return const Center(child: CircularProgressIndicator()); 80 | } else if (state.planetPage.results.isEmpty) { 81 | return const Center(child: Text('No planets found')); 82 | } else if (state.errorFetchingPlanets != null) { 83 | return Center( 84 | child: _Error( 85 | errorMsg: state.errorFetchingPlanets, 86 | ), 87 | ); 88 | } else { 89 | return _PlanetList(); 90 | } 91 | } 92 | } 93 | 94 | class _Error extends StatelessWidget { 95 | const _Error({ 96 | Key? key, 97 | required this.errorMsg, 98 | }) : super(key: key); 99 | 100 | final String? errorMsg; 101 | 102 | @override 103 | Widget build(BuildContext context) { 104 | return Column( 105 | mainAxisAlignment: MainAxisAlignment.center, 106 | children: [ 107 | if (errorMsg != null) Text(errorMsg!), 108 | ElevatedButton( 109 | style: ButtonStyle( 110 | backgroundColor: WidgetStateProperty.all(Colors.redAccent), 111 | ), 112 | onPressed: () async { 113 | await Provider.of<_PlanetHandler>( 114 | context, 115 | listen: false, 116 | ).fetchAndDispatch(); 117 | }, 118 | child: const Text('Try again'), 119 | ), 120 | ], 121 | ); 122 | } 123 | } 124 | 125 | class _LoadPageButton extends HookWidget { 126 | const _LoadPageButton({this.next = true}); 127 | 128 | final bool next; 129 | 130 | @override 131 | Widget build(BuildContext context) { 132 | final state = Provider.of(context); 133 | return ElevatedButton( 134 | onPressed: () async { 135 | final url = next ? state.planetPage.next : state.planetPage.previous; 136 | await Provider.of<_PlanetHandler>(context, listen: false) 137 | .fetchAndDispatch(url); 138 | }, 139 | child: next ? const Text('Next Page') : const Text('Prev Page'), 140 | ); 141 | } 142 | } 143 | 144 | class _PlanetList extends HookWidget { 145 | @override 146 | Widget build(BuildContext context) { 147 | final state = Provider.of(context); 148 | return ListView.builder( 149 | itemCount: 1 + state.planetPage.results.length, 150 | itemBuilder: (context, index) { 151 | if (index == 0) { 152 | return _PlanetListHeader(); 153 | } 154 | 155 | final planet = state.planetPage.results[index - 1]; 156 | return ListTile(title: Text(planet.name)); 157 | }, 158 | ); 159 | } 160 | } 161 | 162 | class _PlanetListHeader extends StatelessWidget { 163 | @override 164 | Widget build(BuildContext context) { 165 | final state = Provider.of(context); 166 | MainAxisAlignment buttonAlignment; 167 | if (state.planetPage.previous == null) { 168 | buttonAlignment = MainAxisAlignment.end; 169 | } else if (state.planetPage.next == null) { 170 | buttonAlignment = MainAxisAlignment.start; 171 | } else { 172 | buttonAlignment = MainAxisAlignment.spaceBetween; 173 | } 174 | 175 | return Row( 176 | mainAxisAlignment: buttonAlignment, 177 | children: [ 178 | if (state.planetPage.previous != null) 179 | const _LoadPageButton(next: false), 180 | if (state.planetPage.next != null) const _LoadPageButton() 181 | ], 182 | ); 183 | } 184 | } 185 | -------------------------------------------------------------------------------- /packages/flutter_hooks/example/lib/star_wars/redux.dart: -------------------------------------------------------------------------------- 1 | import 'package:built_value/built_value.dart'; 2 | import 'package:meta/meta.dart'; 3 | 4 | import 'models.dart'; 5 | 6 | part 'redux.g.dart'; 7 | 8 | /// Actions base class 9 | abstract class ReduxAction {} 10 | 11 | /// Action that updates state to show that we are loading planets 12 | class FetchPlanetPageActionStart extends ReduxAction {} 13 | 14 | /// Action that updates state to show that we are loading planets 15 | class FetchPlanetPageActionError extends ReduxAction { 16 | FetchPlanetPageActionError(this.errorMsg); 17 | 18 | /// Message that should be displayed in the UI 19 | final String errorMsg; 20 | } 21 | 22 | /// Action to set the planet page 23 | class FetchPlanetPageActionSuccess extends ReduxAction { 24 | FetchPlanetPageActionSuccess(this.page); 25 | 26 | final PlanetPageModel page; 27 | } 28 | 29 | @immutable 30 | abstract class AppState implements Built { 31 | factory AppState([void Function(AppStateBuilder)? updates]) => 32 | _$AppState((u) => u 33 | ..isFetchingPlanets = false 34 | ..update(updates)); 35 | 36 | const AppState._(); 37 | 38 | bool get isFetchingPlanets; 39 | 40 | String? get errorFetchingPlanets; 41 | 42 | PlanetPageModel get planetPage; 43 | } 44 | 45 | AppState reducer( 46 | S state, 47 | A action, 48 | ) { 49 | final b = state.toBuilder(); 50 | if (action is FetchPlanetPageActionStart) { 51 | b 52 | ..isFetchingPlanets = true 53 | ..planetPage = PlanetPageModelBuilder() 54 | ..errorFetchingPlanets = null; 55 | } 56 | 57 | if (action is FetchPlanetPageActionError) { 58 | b 59 | ..isFetchingPlanets = false 60 | ..errorFetchingPlanets = action.errorMsg; 61 | } 62 | 63 | if (action is FetchPlanetPageActionSuccess) { 64 | b 65 | ..isFetchingPlanets = false 66 | ..planetPage.replace(action.page); 67 | } 68 | 69 | return b.build(); 70 | } 71 | -------------------------------------------------------------------------------- /packages/flutter_hooks/example/lib/star_wars/redux.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'redux.dart'; 4 | 5 | // ************************************************************************** 6 | // BuiltValueGenerator 7 | // ************************************************************************** 8 | 9 | class _$AppState extends AppState { 10 | @override 11 | final bool isFetchingPlanets; 12 | @override 13 | final String? errorFetchingPlanets; 14 | @override 15 | final PlanetPageModel planetPage; 16 | 17 | factory _$AppState([void Function(AppStateBuilder)? updates]) => 18 | (new AppStateBuilder()..update(updates))._build(); 19 | 20 | _$AppState._( 21 | {required this.isFetchingPlanets, 22 | this.errorFetchingPlanets, 23 | required this.planetPage}) 24 | : super._() { 25 | BuiltValueNullFieldError.checkNotNull( 26 | isFetchingPlanets, r'AppState', 'isFetchingPlanets'); 27 | BuiltValueNullFieldError.checkNotNull( 28 | planetPage, r'AppState', 'planetPage'); 29 | } 30 | 31 | @override 32 | AppState rebuild(void Function(AppStateBuilder) updates) => 33 | (toBuilder()..update(updates)).build(); 34 | 35 | @override 36 | AppStateBuilder toBuilder() => new AppStateBuilder()..replace(this); 37 | 38 | @override 39 | bool operator ==(Object other) { 40 | if (identical(other, this)) return true; 41 | return other is AppState && 42 | isFetchingPlanets == other.isFetchingPlanets && 43 | errorFetchingPlanets == other.errorFetchingPlanets && 44 | planetPage == other.planetPage; 45 | } 46 | 47 | @override 48 | int get hashCode { 49 | var _$hash = 0; 50 | _$hash = $jc(_$hash, isFetchingPlanets.hashCode); 51 | _$hash = $jc(_$hash, errorFetchingPlanets.hashCode); 52 | _$hash = $jc(_$hash, planetPage.hashCode); 53 | _$hash = $jf(_$hash); 54 | return _$hash; 55 | } 56 | 57 | @override 58 | String toString() { 59 | return (newBuiltValueToStringHelper(r'AppState') 60 | ..add('isFetchingPlanets', isFetchingPlanets) 61 | ..add('errorFetchingPlanets', errorFetchingPlanets) 62 | ..add('planetPage', planetPage)) 63 | .toString(); 64 | } 65 | } 66 | 67 | class AppStateBuilder implements Builder { 68 | _$AppState? _$v; 69 | 70 | bool? _isFetchingPlanets; 71 | bool? get isFetchingPlanets => _$this._isFetchingPlanets; 72 | set isFetchingPlanets(bool? isFetchingPlanets) => 73 | _$this._isFetchingPlanets = isFetchingPlanets; 74 | 75 | String? _errorFetchingPlanets; 76 | String? get errorFetchingPlanets => _$this._errorFetchingPlanets; 77 | set errorFetchingPlanets(String? errorFetchingPlanets) => 78 | _$this._errorFetchingPlanets = errorFetchingPlanets; 79 | 80 | PlanetPageModelBuilder? _planetPage; 81 | PlanetPageModelBuilder get planetPage => 82 | _$this._planetPage ??= new PlanetPageModelBuilder(); 83 | set planetPage(PlanetPageModelBuilder? planetPage) => 84 | _$this._planetPage = planetPage; 85 | 86 | AppStateBuilder(); 87 | 88 | AppStateBuilder get _$this { 89 | final $v = _$v; 90 | if ($v != null) { 91 | _isFetchingPlanets = $v.isFetchingPlanets; 92 | _errorFetchingPlanets = $v.errorFetchingPlanets; 93 | _planetPage = $v.planetPage.toBuilder(); 94 | _$v = null; 95 | } 96 | return this; 97 | } 98 | 99 | @override 100 | void replace(AppState other) { 101 | ArgumentError.checkNotNull(other, 'other'); 102 | _$v = other as _$AppState; 103 | } 104 | 105 | @override 106 | void update(void Function(AppStateBuilder)? updates) { 107 | if (updates != null) updates(this); 108 | } 109 | 110 | @override 111 | AppState build() => _build(); 112 | 113 | _$AppState _build() { 114 | _$AppState _$result; 115 | try { 116 | _$result = _$v ?? 117 | new _$AppState._( 118 | isFetchingPlanets: BuiltValueNullFieldError.checkNotNull( 119 | isFetchingPlanets, r'AppState', 'isFetchingPlanets'), 120 | errorFetchingPlanets: errorFetchingPlanets, 121 | planetPage: planetPage.build()); 122 | } catch (_) { 123 | late String _$failedField; 124 | try { 125 | _$failedField = 'planetPage'; 126 | planetPage.build(); 127 | } catch (e) { 128 | throw new BuiltValueNestedFieldError( 129 | r'AppState', _$failedField, e.toString()); 130 | } 131 | rethrow; 132 | } 133 | replace(_$result); 134 | return _$result; 135 | } 136 | } 137 | 138 | // ignore_for_file: deprecated_member_use_from_same_package,type=lint 139 | -------------------------------------------------------------------------------- /packages/flutter_hooks/example/lib/star_wars/star_wars_api.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:http/http.dart' as http; 4 | 5 | import 'models.dart'; 6 | 7 | /// Api wrapper to retrieve Star Wars related data 8 | class StarWarsApi { 9 | /// load and return one page of planets 10 | Future getPlanets([String? page]) async { 11 | page ??= 'https://swapi.dev/api/planets'; 12 | 13 | final response = await http.get(Uri.parse(page)); 14 | final dynamic json = jsonDecode(utf8.decode(response.bodyBytes)); 15 | 16 | return serializers.deserializeWith(PlanetPageModel.serializer, json)!; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /packages/flutter_hooks/example/lib/use_effect.dart: -------------------------------------------------------------------------------- 1 | // ignore_for_file: omit_local_variable_types 2 | import 'dart:async'; 3 | 4 | import 'package:flutter/material.dart'; 5 | import 'package:flutter_hooks/flutter_hooks.dart'; 6 | import 'package:shared_preferences/shared_preferences.dart'; 7 | 8 | /// This example demonstrates how to create a custom Hook. 9 | class CustomHookExample extends HookWidget { 10 | @override 11 | Widget build(BuildContext context) { 12 | // Consume the custom hook. It returns a StreamController that we can use 13 | // within this Widget. 14 | // 15 | // To update the stored value, `add` data to the StreamController. To get 16 | // the latest value from the StreamController, listen to the Stream with 17 | // the useStream hook. 18 | // ignore: close_sinks 19 | final StreamController countController = 20 | _useLocalStorageInt('counter'); 21 | 22 | return Scaffold( 23 | appBar: AppBar( 24 | title: const Text('Custom Hook example'), 25 | ), 26 | body: Center( 27 | // Use a HookBuilder Widget to listen to the Stream. This ensures a 28 | // smaller portion of the Widget tree is rebuilt when the stream emits a 29 | // new value 30 | child: HookBuilder( 31 | builder: (context) { 32 | final AsyncSnapshot count = 33 | useStream(countController.stream, initialData: 0); 34 | 35 | return !count.hasData 36 | ? const CircularProgressIndicator() 37 | : GestureDetector( 38 | onTap: () => countController.add(count.requireData + 1), 39 | child: Text('You tapped me ${count.data} times.'), 40 | ); 41 | }, 42 | ), 43 | ), 44 | ); 45 | } 46 | } 47 | 48 | // A custom hook that will read and write values to local storage using the 49 | // SharedPreferences package. 50 | StreamController _useLocalStorageInt( 51 | String key, { 52 | int defaultValue = 0, 53 | }) { 54 | // Custom hooks can use additional hooks internally! 55 | final controller = useStreamController(keys: [key]); 56 | 57 | // Pass a callback to the useEffect hook. This function should be called on 58 | // first build and every time the controller or key changes 59 | useEffect( 60 | () { 61 | // Listen to the StreamController, and when a value is added, store it 62 | // using SharedPrefs. 63 | final sub = controller.stream.listen((data) async { 64 | final prefs = await SharedPreferences.getInstance(); 65 | await prefs.setInt(key, data); 66 | }); 67 | // Unsubscribe when the widget is disposed 68 | // or on controller/key change 69 | return sub.cancel; 70 | }, 71 | // Pass the controller and key to the useEffect hook. This will ensure the 72 | // useEffect hook is only called the first build or when one of the the 73 | // values changes. 74 | [controller, key], 75 | ); 76 | 77 | // Load the initial value from local storage and add it as the initial value 78 | // to the controller 79 | useEffect( 80 | () { 81 | SharedPreferences.getInstance().then((prefs) async { 82 | final int? valueFromStorage = prefs.getInt(key); 83 | controller.add(valueFromStorage ?? defaultValue); 84 | }).catchError(controller.addError); 85 | return null; 86 | }, 87 | // Pass the controller and key to the useEffect hook. This will ensure the 88 | // useEffect hook is only called the first build or when one of the the 89 | // values changes. 90 | [controller, key], 91 | ); 92 | 93 | // Finally, return the StreamController. This allows users to add values from 94 | // the Widget layer and listen to the stream for changes. 95 | return controller; 96 | } 97 | -------------------------------------------------------------------------------- /packages/flutter_hooks/example/lib/use_reducer.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_hooks/flutter_hooks.dart'; 3 | 4 | /// This example emulates the basic Counter app generated by the 5 | /// `flutter create` command to demonstrates the `useReducer` hook. 6 | /// 7 | /// First, instead of a StatefulWidget, use a HookWidget instead! 8 | 9 | @immutable 10 | class State { 11 | const State({this.counter = 0}); 12 | final int counter; 13 | } 14 | 15 | // Create the actions you wish to dispatch to the reducer 16 | class IncrementCounter { 17 | IncrementCounter({required this.counter}); 18 | 19 | final int counter; 20 | } 21 | 22 | class UseReducerExample extends HookWidget { 23 | @override 24 | Widget build(BuildContext context) { 25 | // Create the reducer function that will handle the actions you dispatch 26 | State _reducer(State state, IncrementCounter? action) { 27 | if (action is IncrementCounter) { 28 | return State(counter: state.counter + action.counter); 29 | } 30 | return state; 31 | } 32 | 33 | // Next, invoke the `useReducer` function with the reducer function and initial state to create a 34 | // `_store` variable that contains the current state and dispatch. Whenever the value is 35 | // changed, this Widget will be rebuilt! 36 | final _store = useReducer( 37 | _reducer, 38 | initialState: const State(), 39 | initialAction: null, 40 | ); 41 | 42 | return Scaffold( 43 | appBar: AppBar( 44 | title: const Text('useState example'), 45 | ), 46 | body: Center( 47 | // Read the current value from the counter 48 | child: Text('Button tapped ${_store.state.counter} times'), 49 | ), 50 | floatingActionButton: FloatingActionButton( 51 | // When the button is pressed, dispatch the Action you wish to trigger! This 52 | // will trigger a rebuild, displaying the latest value in the Text 53 | // Widget above! 54 | onPressed: () => _store.dispatch(IncrementCounter(counter: 1)), 55 | child: const Icon(Icons.add), 56 | ), 57 | ); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /packages/flutter_hooks/example/lib/use_state.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_hooks/flutter_hooks.dart'; 3 | 4 | /// This example emulates the basic Counter app generated by the 5 | /// `flutter create` command to demonstrates the `useState` hook. 6 | /// 7 | /// First, instead of a StatefulWidget, use a HookWidget instead! 8 | class UseStateExample extends HookWidget { 9 | @override 10 | Widget build(BuildContext context) { 11 | // Next, invoke the `useState` function with a default value to create a 12 | // `counter` variable that contains a `value`. Whenever the value is 13 | // changed, this Widget will be rebuilt! 14 | final counter = useState(0); 15 | 16 | return Scaffold( 17 | appBar: AppBar( 18 | title: const Text('useState example'), 19 | ), 20 | body: Center( 21 | // Read the current value from the counter 22 | child: Text('Button tapped ${counter.value} times'), 23 | ), 24 | floatingActionButton: FloatingActionButton( 25 | // When the button is pressed, update the value of the counter! This 26 | // will trigger a rebuild, displaying the latest value in the Text 27 | // Widget above! 28 | onPressed: () => counter.value++, 29 | child: const Icon(Icons.add), 30 | ), 31 | ); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /packages/flutter_hooks/example/lib/use_stream.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutter_hooks/flutter_hooks.dart'; 5 | 6 | /// This example demonstrates how to use Hooks to rebuild a Widget whenever 7 | /// a Stream emits a new value. 8 | class UseStreamExample extends StatelessWidget { 9 | @override 10 | Widget build(BuildContext context) { 11 | return Scaffold( 12 | appBar: AppBar( 13 | title: const Text('useStream example'), 14 | ), 15 | body: Center( 16 | // In this example, the Text Widget is the only portion that needs to 17 | // rebuild when the Stream changes. Therefore, use a HookBuilder 18 | // Widget to limit rebuilds to this section of the app, rather than 19 | // marking the entire UseStreamExample as a HookWidget! 20 | child: HookBuilder( 21 | builder: (context) { 22 | // First, create and cache a Stream with the `useMemoized` hook. 23 | // This hook allows you to create an Object (such as a Stream or 24 | // Future) the first time this builder function is invoked without 25 | // recreating it on each subsequent build! 26 | final stream = useMemoized( 27 | () => Stream.periodic( 28 | const Duration(seconds: 1), (i) => i + 1), 29 | ); 30 | // Next, invoke the `useStream` hook to listen for updates to the 31 | // Stream. This triggers a rebuild whenever a new value is emitted. 32 | // 33 | // Like normal StreamBuilders, it returns the current AsyncSnapshot. 34 | final snapshot = useStream(stream, initialData: 0); 35 | 36 | // Finally, use the data from the Stream to render a text Widget. 37 | // If no data is available, fallback to a default value. 38 | return Text( 39 | '${snapshot.data ?? 0}', 40 | style: const TextStyle(fontSize: 36), 41 | ); 42 | }, 43 | ), 44 | ), 45 | ); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /packages/flutter_hooks/example/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: flutter_hooks_gallery 2 | description: A new Flutter project. 3 | 4 | publish_to: none 5 | 6 | environment: 7 | sdk: ">=2.12.0 <4.0.0" 8 | 9 | dependencies: 10 | built_collection: ^5.0.0 11 | built_value: ^8.0.0 12 | flutter: 13 | sdk: flutter 14 | flutter_hooks: 15 | path: ../ 16 | http: ^0.13.4 17 | provider: ^6.0.5 18 | shared_preferences: ^2.0.0 19 | 20 | dev_dependencies: 21 | build_runner: ^2.1.11 22 | built_value_generator: ^8.3.3 23 | flutter_test: 24 | sdk: flutter 25 | 26 | flutter: 27 | uses-material-design: true 28 | -------------------------------------------------------------------------------- /packages/flutter_hooks/flutter-hook.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/flutter_hooks/lib/flutter_hooks.dart: -------------------------------------------------------------------------------- 1 | export 'package:flutter_hooks/src/framework.dart'; 2 | export 'package:flutter_hooks/src/hooks.dart'; 3 | -------------------------------------------------------------------------------- /packages/flutter_hooks/lib/src/carousel_controller.dart: -------------------------------------------------------------------------------- 1 | part of 'hooks.dart'; 2 | 3 | /// Creates a [CarouselController] that will be disposed automatically. 4 | /// 5 | /// See also: 6 | /// - [CarouselController] 7 | CarouselController useCarouselController({ 8 | int initialItem = 0, 9 | List? keys, 10 | }) { 11 | return use( 12 | _CarouselControllerHook( 13 | initialItem: initialItem, 14 | keys: keys, 15 | ), 16 | ); 17 | } 18 | 19 | class _CarouselControllerHook extends Hook { 20 | const _CarouselControllerHook({ 21 | required this.initialItem, 22 | super.keys, 23 | }); 24 | 25 | final int initialItem; 26 | 27 | @override 28 | HookState> createState() => 29 | _CarouselControllerHookState(); 30 | } 31 | 32 | class _CarouselControllerHookState 33 | extends HookState { 34 | late final controller = CarouselController( 35 | initialItem: hook.initialItem, 36 | ); 37 | 38 | @override 39 | CarouselController build(BuildContext context) => controller; 40 | 41 | @override 42 | void dispose() => controller.dispose(); 43 | 44 | @override 45 | String get debugLabel => 'useCarouselController'; 46 | } 47 | -------------------------------------------------------------------------------- /packages/flutter_hooks/lib/src/debounced.dart: -------------------------------------------------------------------------------- 1 | part of 'hooks.dart'; 2 | 3 | /// Returns a debounced version of the provided value [toDebounce], triggering 4 | /// widget updates accordingly after a specified [timeout] duration. 5 | /// 6 | /// Example: 7 | /// ```dart 8 | /// String userInput = ''; // Your input value 9 | /// 10 | /// // Create a debounced version of userInput 11 | /// final debouncedInput = useDebounced( 12 | /// userInput, 13 | /// Duration(milliseconds: 500), // Set your desired timeout 14 | /// ); 15 | /// // Assume a fetch method fetchData(String query) exists 16 | /// useEffect(() { 17 | /// fetchData(debouncedInput); // Use debouncedInput as a dependency 18 | /// return null; 19 | /// }, [debouncedInput]); 20 | /// ``` 21 | T? useDebounced( 22 | T toDebounce, 23 | Duration timeout, 24 | ) { 25 | return use( 26 | _DebouncedHook( 27 | toDebounce: toDebounce, 28 | timeout: timeout, 29 | ), 30 | ); 31 | } 32 | 33 | class _DebouncedHook extends Hook { 34 | const _DebouncedHook({ 35 | required this.toDebounce, 36 | required this.timeout, 37 | }); 38 | 39 | final T toDebounce; 40 | final Duration timeout; 41 | 42 | @override 43 | _DebouncedHookState createState() => _DebouncedHookState(); 44 | } 45 | 46 | class _DebouncedHookState extends HookState> { 47 | T? _state; 48 | Timer? _timer; 49 | 50 | @override 51 | void initHook() { 52 | super.initHook(); 53 | _startDebounce(hook.toDebounce); 54 | } 55 | 56 | void _startDebounce(T toDebounce) { 57 | _timer?.cancel(); 58 | _timer = Timer(hook.timeout, () { 59 | setState(() { 60 | _state = toDebounce; 61 | }); 62 | }); 63 | } 64 | 65 | @override 66 | void didUpdateHook(_DebouncedHook oldHook) { 67 | if (hook.toDebounce != oldHook.toDebounce || 68 | hook.timeout != oldHook.timeout) { 69 | _startDebounce(hook.toDebounce); 70 | } 71 | } 72 | 73 | @override 74 | T? build(BuildContext context) => _state; 75 | 76 | @override 77 | Object? get debugValue => _state; 78 | 79 | @override 80 | String get debugLabel => 'useDebounced<$T>'; 81 | 82 | @override 83 | void dispose() { 84 | _timer?.cancel(); 85 | _timer = null; 86 | super.dispose(); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /packages/flutter_hooks/lib/src/draggable_scrollable_controller.dart: -------------------------------------------------------------------------------- 1 | part of 'hooks.dart'; 2 | 3 | /// Creates a [DraggableScrollableController] that will be disposed automatically. 4 | /// 5 | /// See also: 6 | /// - [DraggableScrollableController] 7 | DraggableScrollableController useDraggableScrollableController({ 8 | List? keys, 9 | }) { 10 | return use(_DraggableScrollableControllerHook(keys: keys)); 11 | } 12 | 13 | class _DraggableScrollableControllerHook 14 | extends Hook { 15 | const _DraggableScrollableControllerHook({super.keys}); 16 | 17 | @override 18 | HookState> 19 | createState() => _DraggableScrollableControllerHookState(); 20 | } 21 | 22 | class _DraggableScrollableControllerHookState extends HookState< 23 | DraggableScrollableController, _DraggableScrollableControllerHook> { 24 | final controller = DraggableScrollableController(); 25 | 26 | @override 27 | String get debugLabel => 'useDraggableScrollableController'; 28 | 29 | @override 30 | DraggableScrollableController build(BuildContext context) => controller; 31 | 32 | @override 33 | void dispose() => controller.dispose(); 34 | } 35 | -------------------------------------------------------------------------------- /packages/flutter_hooks/lib/src/expansion_tile_controller.dart: -------------------------------------------------------------------------------- 1 | part of 'hooks.dart'; 2 | 3 | /// Creates a [ExpansibleController] that will be disposed automatically. 4 | /// 5 | /// See also: 6 | /// - [ExpansibleController] 7 | ExpansibleController useExpansibleController({List? keys}) { 8 | return use(_ExpansibleControllerHook(keys: keys)); 9 | } 10 | 11 | /// Creates a [ExpansionTileController] that will be disposed automatically. 12 | /// 13 | /// See also: 14 | /// - [ExpansionTileController] 15 | @Deprecated('Use `useExpansibleController` instead.') 16 | ExpansionTileController useExpansionTileController({List? keys}) { 17 | return use(_ExpansibleControllerHook(keys: keys)); 18 | } 19 | 20 | class _ExpansibleControllerHook extends Hook { 21 | const _ExpansibleControllerHook({List? keys}) : super(keys: keys); 22 | 23 | @override 24 | HookState> createState() => 25 | _ExpansibleControllerHookState(); 26 | } 27 | 28 | class _ExpansibleControllerHookState 29 | extends HookState { 30 | final controller = ExpansibleController(); 31 | 32 | @override 33 | String get debugLabel => 'useExpansibleController'; 34 | 35 | @override 36 | ExpansibleController build(BuildContext context) => controller; 37 | } 38 | -------------------------------------------------------------------------------- /packages/flutter_hooks/lib/src/fixed_extent_scroll_controller.dart: -------------------------------------------------------------------------------- 1 | part of 'hooks.dart'; 2 | 3 | /// Creates [FixedExtentScrollController] that will be disposed automatically. 4 | /// 5 | /// See also: 6 | /// - [FixedExtentScrollController] 7 | FixedExtentScrollController useFixedExtentScrollController({ 8 | int initialItem = 0, 9 | ScrollControllerCallback? onAttach, 10 | ScrollControllerCallback? onDetach, 11 | List? keys, 12 | }) { 13 | return use( 14 | _FixedExtentScrollControllerHook( 15 | initialItem: initialItem, 16 | onAttach: onAttach, 17 | onDetach: onDetach, 18 | keys: keys, 19 | ), 20 | ); 21 | } 22 | 23 | class _FixedExtentScrollControllerHook 24 | extends Hook { 25 | const _FixedExtentScrollControllerHook({ 26 | required this.initialItem, 27 | this.onAttach, 28 | this.onDetach, 29 | super.keys, 30 | }); 31 | 32 | final int initialItem; 33 | final ScrollControllerCallback? onAttach; 34 | final ScrollControllerCallback? onDetach; 35 | 36 | @override 37 | HookState> 38 | createState() => _FixedExtentScrollControllerHookState(); 39 | } 40 | 41 | class _FixedExtentScrollControllerHookState extends HookState< 42 | FixedExtentScrollController, _FixedExtentScrollControllerHook> { 43 | late final controller = FixedExtentScrollController( 44 | initialItem: hook.initialItem, 45 | onAttach: hook.onAttach, 46 | onDetach: hook.onDetach, 47 | ); 48 | 49 | @override 50 | FixedExtentScrollController build(BuildContext context) => controller; 51 | 52 | @override 53 | void dispose() => controller.dispose(); 54 | 55 | @override 56 | String get debugLabel => 'useFixedExtentScrollController'; 57 | } 58 | -------------------------------------------------------------------------------- /packages/flutter_hooks/lib/src/focus_node.dart: -------------------------------------------------------------------------------- 1 | part of 'hooks.dart'; 2 | 3 | /// Creates an automatically disposed [FocusNode]. 4 | /// 5 | /// See also: 6 | /// - [FocusNode] 7 | FocusNode useFocusNode({ 8 | String? debugLabel, 9 | FocusOnKeyEventCallback? onKeyEvent, 10 | bool skipTraversal = false, 11 | bool canRequestFocus = true, 12 | bool descendantsAreFocusable = true, 13 | }) { 14 | return use( 15 | _FocusNodeHook( 16 | debugLabel: debugLabel, 17 | onKeyEvent: onKeyEvent, 18 | skipTraversal: skipTraversal, 19 | canRequestFocus: canRequestFocus, 20 | descendantsAreFocusable: descendantsAreFocusable, 21 | ), 22 | ); 23 | } 24 | 25 | class _FocusNodeHook extends Hook { 26 | const _FocusNodeHook({ 27 | this.debugLabel, 28 | this.onKeyEvent, 29 | required this.skipTraversal, 30 | required this.canRequestFocus, 31 | required this.descendantsAreFocusable, 32 | }); 33 | 34 | final String? debugLabel; 35 | final FocusOnKeyEventCallback? onKeyEvent; 36 | final bool skipTraversal; 37 | final bool canRequestFocus; 38 | final bool descendantsAreFocusable; 39 | 40 | @override 41 | _FocusNodeHookState createState() { 42 | return _FocusNodeHookState(); 43 | } 44 | } 45 | 46 | class _FocusNodeHookState extends HookState { 47 | late final FocusNode _focusNode = FocusNode( 48 | debugLabel: hook.debugLabel, 49 | onKeyEvent: hook.onKeyEvent, 50 | skipTraversal: hook.skipTraversal, 51 | canRequestFocus: hook.canRequestFocus, 52 | descendantsAreFocusable: hook.descendantsAreFocusable, 53 | ); 54 | 55 | @override 56 | void didUpdateHook(_FocusNodeHook oldHook) { 57 | _focusNode 58 | ..debugLabel = hook.debugLabel 59 | ..skipTraversal = hook.skipTraversal 60 | ..canRequestFocus = hook.canRequestFocus 61 | ..descendantsAreFocusable = hook.descendantsAreFocusable 62 | ..onKeyEvent = hook.onKeyEvent; 63 | } 64 | 65 | @override 66 | FocusNode build(BuildContext context) => _focusNode; 67 | 68 | @override 69 | void dispose() => _focusNode.dispose(); 70 | 71 | @override 72 | String get debugLabel => 'useFocusNode'; 73 | } 74 | -------------------------------------------------------------------------------- /packages/flutter_hooks/lib/src/focus_scope_node.dart: -------------------------------------------------------------------------------- 1 | part of 'hooks.dart'; 2 | 3 | /// Creates an automatically disposed [FocusScopeNode]. 4 | /// 5 | /// See also: 6 | /// - [FocusScopeNode] 7 | FocusScopeNode useFocusScopeNode({ 8 | String? debugLabel, 9 | FocusOnKeyEventCallback? onKeyEvent, 10 | bool skipTraversal = false, 11 | bool canRequestFocus = true, 12 | }) { 13 | return use( 14 | _FocusScopeNodeHook( 15 | debugLabel: debugLabel, 16 | onKeyEvent: onKeyEvent, 17 | skipTraversal: skipTraversal, 18 | canRequestFocus: canRequestFocus, 19 | ), 20 | ); 21 | } 22 | 23 | class _FocusScopeNodeHook extends Hook { 24 | const _FocusScopeNodeHook({ 25 | this.debugLabel, 26 | this.onKeyEvent, 27 | required this.skipTraversal, 28 | required this.canRequestFocus, 29 | }); 30 | 31 | final String? debugLabel; 32 | final FocusOnKeyEventCallback? onKeyEvent; 33 | final bool skipTraversal; 34 | final bool canRequestFocus; 35 | 36 | @override 37 | _FocusScopeNodeHookState createState() { 38 | return _FocusScopeNodeHookState(); 39 | } 40 | } 41 | 42 | class _FocusScopeNodeHookState 43 | extends HookState { 44 | late final FocusScopeNode _focusScopeNode = FocusScopeNode( 45 | debugLabel: hook.debugLabel, 46 | onKeyEvent: hook.onKeyEvent, 47 | skipTraversal: hook.skipTraversal, 48 | canRequestFocus: hook.canRequestFocus, 49 | ); 50 | 51 | @override 52 | void didUpdateHook(_FocusScopeNodeHook oldHook) { 53 | _focusScopeNode 54 | ..debugLabel = hook.debugLabel 55 | ..skipTraversal = hook.skipTraversal 56 | ..canRequestFocus = hook.canRequestFocus 57 | ..onKeyEvent = hook.onKeyEvent; 58 | } 59 | 60 | @override 61 | FocusScopeNode build(BuildContext context) => _focusScopeNode; 62 | 63 | @override 64 | void dispose() => _focusScopeNode.dispose(); 65 | 66 | @override 67 | String get debugLabel => 'useFocusScopeNode'; 68 | } 69 | -------------------------------------------------------------------------------- /packages/flutter_hooks/lib/src/hooks.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:flutter/foundation.dart'; 4 | import 'package:flutter/material.dart' 5 | show 6 | Brightness, 7 | CarouselController, 8 | DraggableScrollableController, 9 | // ignore: deprecated_member_use 10 | ExpansionTileController, 11 | SearchController, 12 | TabController, 13 | WidgetState, 14 | WidgetStatesController, 15 | kTabScrollDuration; 16 | import 'package:flutter/scheduler.dart'; 17 | import 'package:flutter/widgets.dart'; 18 | 19 | import 'framework.dart'; 20 | 21 | part 'animation.dart'; 22 | part 'async.dart'; 23 | part 'carousel_controller.dart'; 24 | part 'debounced.dart'; 25 | part 'draggable_scrollable_controller.dart'; 26 | part 'expansion_tile_controller.dart'; 27 | part 'fixed_extent_scroll_controller.dart'; 28 | part 'focus_node.dart'; 29 | part 'focus_scope_node.dart'; 30 | part 'keep_alive.dart'; 31 | part 'listenable.dart'; 32 | part 'listenable_selector.dart'; 33 | part 'misc.dart'; 34 | part 'page_controller.dart'; 35 | part 'platform_brightness.dart'; 36 | part 'primitives.dart'; 37 | part 'scroll_controller.dart'; 38 | part 'search_controller.dart'; 39 | part 'tab_controller.dart'; 40 | part 'text_controller.dart'; 41 | part 'transformation_controller.dart'; 42 | part 'tree_sliver_controller.dart'; 43 | part 'widget_states_controller.dart'; 44 | part 'widgets_binding_observer.dart'; 45 | -------------------------------------------------------------------------------- /packages/flutter_hooks/lib/src/keep_alive.dart: -------------------------------------------------------------------------------- 1 | part of 'hooks.dart'; 2 | 3 | /// Mark a widget using this hook as needing to stay alive even when it's in a 4 | /// lazy list that would otherwise remove it. 5 | /// 6 | /// See also: 7 | /// - [AutomaticKeepAlive] 8 | /// - [KeepAlive] 9 | void useAutomaticKeepAlive({ 10 | bool wantKeepAlive = true, 11 | }) { 12 | use(_AutomaticKeepAliveHook( 13 | wantKeepAlive: wantKeepAlive, 14 | )); 15 | } 16 | 17 | class _AutomaticKeepAliveHook extends Hook { 18 | const _AutomaticKeepAliveHook({required this.wantKeepAlive}); 19 | 20 | final bool wantKeepAlive; 21 | 22 | @override 23 | HookState createState() => 24 | _AutomaticKeepAliveHookState(); 25 | } 26 | 27 | class _AutomaticKeepAliveHookState 28 | extends HookState { 29 | KeepAliveHandle? _keepAliveHandle; 30 | 31 | void _ensureKeepAlive() { 32 | _keepAliveHandle = KeepAliveHandle(); 33 | KeepAliveNotification(_keepAliveHandle!).dispatch(context); 34 | } 35 | 36 | void _releaseKeepAlive() { 37 | _keepAliveHandle?.dispose(); 38 | _keepAliveHandle = null; 39 | } 40 | 41 | @override 42 | void initHook() { 43 | super.initHook(); 44 | 45 | if (hook.wantKeepAlive) { 46 | _ensureKeepAlive(); 47 | } 48 | } 49 | 50 | @override 51 | void build(BuildContext context) { 52 | if (hook.wantKeepAlive && _keepAliveHandle == null) { 53 | _ensureKeepAlive(); 54 | } 55 | } 56 | 57 | @override 58 | void deactivate() { 59 | if (_keepAliveHandle != null) { 60 | _releaseKeepAlive(); 61 | } 62 | super.deactivate(); 63 | } 64 | 65 | @override 66 | Object? get debugValue => _keepAliveHandle; 67 | 68 | @override 69 | String get debugLabel => 'useAutomaticKeepAlive'; 70 | } 71 | -------------------------------------------------------------------------------- /packages/flutter_hooks/lib/src/listenable.dart: -------------------------------------------------------------------------------- 1 | part of 'hooks.dart'; 2 | 3 | /// Subscribes to a [ValueListenable] and returns its value. 4 | /// 5 | /// See also: 6 | /// * [ValueListenable], the created object 7 | /// * [useListenable] 8 | T useValueListenable(ValueListenable valueListenable) { 9 | use(_UseValueListenableHook(valueListenable)); 10 | return valueListenable.value; 11 | } 12 | 13 | class _UseValueListenableHook extends _ListenableHook { 14 | const _UseValueListenableHook(ValueListenable animation) 15 | : super(animation); 16 | 17 | @override 18 | _UseValueListenableStateHook createState() { 19 | return _UseValueListenableStateHook(); 20 | } 21 | } 22 | 23 | class _UseValueListenableStateHook extends _ListenableStateHook { 24 | @override 25 | String get debugLabel => 'useValueListenable'; 26 | 27 | @override 28 | Object? get debugValue => (hook.listenable as ValueListenable?)?.value; 29 | } 30 | 31 | /// Subscribes to a [Listenable] and marks the widget as needing build 32 | /// whenever the listener is called. 33 | /// 34 | /// See also: 35 | /// * [Listenable] 36 | /// * [useValueListenable], [useAnimation] 37 | T useListenable(T listenable) { 38 | use(_ListenableHook(listenable)); 39 | return listenable; 40 | } 41 | 42 | class _ListenableHook extends Hook { 43 | const _ListenableHook(this.listenable); 44 | 45 | final Listenable? listenable; 46 | 47 | @override 48 | _ListenableStateHook createState() => _ListenableStateHook(); 49 | } 50 | 51 | class _ListenableStateHook extends HookState { 52 | @override 53 | void initHook() { 54 | super.initHook(); 55 | hook.listenable?.addListener(_listener); 56 | } 57 | 58 | @override 59 | void didUpdateHook(_ListenableHook oldHook) { 60 | super.didUpdateHook(oldHook); 61 | if (hook.listenable != oldHook.listenable) { 62 | oldHook.listenable?.removeListener(_listener); 63 | hook.listenable?.addListener(_listener); 64 | } 65 | } 66 | 67 | @override 68 | void build(BuildContext context) {} 69 | 70 | void _listener() { 71 | setState(() {}); 72 | } 73 | 74 | @override 75 | void dispose() { 76 | hook.listenable?.removeListener(_listener); 77 | } 78 | 79 | @override 80 | String get debugLabel => 'useListenable'; 81 | 82 | @override 83 | Object? get debugValue => hook.listenable; 84 | } 85 | 86 | /// Creates a [ValueNotifier] that is automatically disposed. 87 | /// 88 | /// As opposed to `useState`, this hook does not subscribe to [ValueNotifier]. 89 | /// This allows a more granular rebuild. 90 | /// 91 | /// See also: 92 | /// * [ValueNotifier] 93 | /// * [useValueListenable] 94 | ValueNotifier useValueNotifier(T initialData, [List? keys]) { 95 | return use( 96 | _ValueNotifierHook( 97 | initialData: initialData, 98 | keys: keys, 99 | ), 100 | ); 101 | } 102 | 103 | class _ValueNotifierHook extends Hook> { 104 | const _ValueNotifierHook({List? keys, required this.initialData}) 105 | : super(keys: keys); 106 | 107 | final T initialData; 108 | 109 | @override 110 | _UseValueNotifierHookState createState() => 111 | _UseValueNotifierHookState(); 112 | } 113 | 114 | class _UseValueNotifierHookState 115 | extends HookState, _ValueNotifierHook> { 116 | late final notifier = ValueNotifier(hook.initialData); 117 | 118 | @override 119 | ValueNotifier build(BuildContext context) { 120 | return notifier; 121 | } 122 | 123 | @override 124 | void dispose() { 125 | notifier.dispose(); 126 | } 127 | 128 | @override 129 | String get debugLabel => 'useValueNotifier'; 130 | } 131 | 132 | /// Adds a given [listener] to a [Listenable] and removes it when the hook is 133 | /// disposed. 134 | /// 135 | /// As opposed to `useListenable`, this hook does not mark the widget as needing 136 | /// build when the listener is called. Use this for side effects that do not 137 | /// require a rebuild. 138 | /// 139 | /// See also: 140 | /// * [Listenable] 141 | /// * [ValueListenable] 142 | /// * [useListenable] 143 | void useOnListenableChange( 144 | Listenable? listenable, 145 | VoidCallback listener, 146 | ) { 147 | return use(_OnListenableChangeHook(listenable, listener)); 148 | } 149 | 150 | class _OnListenableChangeHook extends Hook { 151 | const _OnListenableChangeHook( 152 | this.listenable, 153 | this.listener, 154 | ); 155 | 156 | final Listenable? listenable; 157 | final VoidCallback listener; 158 | 159 | @override 160 | _OnListenableChangeHookState createState() => _OnListenableChangeHookState(); 161 | } 162 | 163 | class _OnListenableChangeHookState 164 | extends HookState { 165 | @override 166 | void initHook() { 167 | super.initHook(); 168 | hook.listenable?.addListener(_listener); 169 | } 170 | 171 | @override 172 | void didUpdateHook(_OnListenableChangeHook oldHook) { 173 | super.didUpdateHook(oldHook); 174 | if (hook.listenable != oldHook.listenable) { 175 | oldHook.listenable?.removeListener(_listener); 176 | hook.listenable?.addListener(_listener); 177 | } 178 | } 179 | 180 | @override 181 | void build(BuildContext context) {} 182 | 183 | @override 184 | void dispose() { 185 | hook.listenable?.removeListener(_listener); 186 | } 187 | 188 | /// Wraps `hook.listener` so we have a non-changing reference to it. 189 | void _listener() { 190 | hook.listener(); 191 | } 192 | 193 | @override 194 | String get debugLabel => 'useOnListenableChange'; 195 | 196 | @override 197 | Object? get debugValue => hook.listenable; 198 | } 199 | -------------------------------------------------------------------------------- /packages/flutter_hooks/lib/src/listenable_selector.dart: -------------------------------------------------------------------------------- 1 | part of 'hooks.dart'; 2 | 3 | /// Rebuild only when there is a change in the selector result. 4 | /// 5 | /// The following example showcases If no text is entered, you will not be able to press the button. 6 | /// ```dart 7 | /// class Example extends HookWidget { 8 | /// @override 9 | /// Widget build(BuildContext context) { 10 | /// final listenable = useTextEditingController(); 11 | /// final bool textIsEmpty = 12 | /// useListenableSelector(listenable, () => listenable.text.isEmpty); 13 | /// return Column( 14 | /// children: [ 15 | /// TextField(controller: listenable), 16 | /// ElevatedButton( 17 | /// // If no text is entered, the button cannot be pressed 18 | /// onPressed: textIsEmpty ? null : () => print("Button can be pressed!"), 19 | /// child: Text("Button")), 20 | /// ], 21 | /// ); 22 | /// } 23 | /// } 24 | /// ``` 25 | /// 26 | 27 | R useListenableSelector( 28 | Listenable? listenable, 29 | R Function() selector, 30 | ) { 31 | return use(_ListenableSelectorHook(listenable, selector)); 32 | } 33 | 34 | class _ListenableSelectorHook extends Hook { 35 | const _ListenableSelectorHook(this.listenable, this.selector); 36 | 37 | final Listenable? listenable; 38 | final R Function() selector; 39 | 40 | @override 41 | _ListenableSelectorHookState createState() => 42 | _ListenableSelectorHookState(); 43 | } 44 | 45 | class _ListenableSelectorHookState 46 | extends HookState> { 47 | late R _selectorResult = hook.selector(); 48 | 49 | @override 50 | void initHook() { 51 | super.initHook(); 52 | hook.listenable?.addListener(_listener); 53 | } 54 | 55 | @override 56 | void didUpdateHook(_ListenableSelectorHook oldHook) { 57 | super.didUpdateHook(oldHook); 58 | 59 | if (hook.selector != oldHook.selector) { 60 | setState(() { 61 | _selectorResult = hook.selector(); 62 | }); 63 | } 64 | 65 | if (hook.listenable != oldHook.listenable) { 66 | oldHook.listenable?.removeListener(_listener); 67 | hook.listenable?.addListener(_listener); 68 | _selectorResult = hook.selector(); 69 | } 70 | } 71 | 72 | @override 73 | R build(BuildContext context) => _selectorResult; 74 | 75 | void _listener() { 76 | final latestSelectorResult = hook.selector(); 77 | if (_selectorResult != latestSelectorResult) { 78 | setState(() { 79 | _selectorResult = latestSelectorResult; 80 | }); 81 | } 82 | } 83 | 84 | @override 85 | void dispose() { 86 | hook.listenable?.removeListener(_listener); 87 | } 88 | 89 | @override 90 | String get debugLabel => 'useListenableSelector<$R>'; 91 | 92 | @override 93 | bool get debugSkipValue => true; 94 | } 95 | -------------------------------------------------------------------------------- /packages/flutter_hooks/lib/src/page_controller.dart: -------------------------------------------------------------------------------- 1 | part of 'hooks.dart'; 2 | 3 | /// Creates a [PageController] that will be disposed automatically. 4 | /// 5 | /// See also: 6 | /// - [PageController] 7 | PageController usePageController({ 8 | int initialPage = 0, 9 | bool keepPage = true, 10 | double viewportFraction = 1.0, 11 | ScrollControllerCallback? onAttach, 12 | ScrollControllerCallback? onDetach, 13 | List? keys, 14 | }) { 15 | return use( 16 | _PageControllerHook( 17 | initialPage: initialPage, 18 | keepPage: keepPage, 19 | viewportFraction: viewportFraction, 20 | onAttach: onAttach, 21 | onDetach: onDetach, 22 | keys: keys, 23 | ), 24 | ); 25 | } 26 | 27 | class _PageControllerHook extends Hook { 28 | const _PageControllerHook({ 29 | required this.initialPage, 30 | required this.keepPage, 31 | required this.viewportFraction, 32 | this.onAttach, 33 | this.onDetach, 34 | List? keys, 35 | }) : super(keys: keys); 36 | 37 | final int initialPage; 38 | final bool keepPage; 39 | final double viewportFraction; 40 | final ScrollControllerCallback? onAttach; 41 | final ScrollControllerCallback? onDetach; 42 | 43 | @override 44 | HookState> createState() => 45 | _PageControllerHookState(); 46 | } 47 | 48 | class _PageControllerHookState 49 | extends HookState { 50 | late final controller = PageController( 51 | initialPage: hook.initialPage, 52 | keepPage: hook.keepPage, 53 | viewportFraction: hook.viewportFraction, 54 | onAttach: hook.onAttach, 55 | onDetach: hook.onDetach, 56 | ); 57 | 58 | @override 59 | PageController build(BuildContext context) => controller; 60 | 61 | @override 62 | void dispose() => controller.dispose(); 63 | 64 | @override 65 | String get debugLabel => 'usePageController'; 66 | } 67 | -------------------------------------------------------------------------------- /packages/flutter_hooks/lib/src/platform_brightness.dart: -------------------------------------------------------------------------------- 1 | part of 'hooks.dart'; 2 | 3 | /// A callback triggered when the platform brightness changes. 4 | typedef BrightnessCallback = FutureOr Function( 5 | Brightness previous, 6 | Brightness current, 7 | ); 8 | 9 | /// Returns the current platform [Brightness] value and rebuilds the widget when it changes. 10 | Brightness usePlatformBrightness() { 11 | return use(const _PlatformBrightnessHook(rebuildOnChange: true)); 12 | } 13 | 14 | /// Listens to the platform [Brightness]. 15 | void useOnPlatformBrightnessChange(BrightnessCallback onBrightnessChange) { 16 | return use(_PlatformBrightnessHook(onBrightnessChange: onBrightnessChange)); 17 | } 18 | 19 | class _PlatformBrightnessHook extends Hook { 20 | const _PlatformBrightnessHook({ 21 | this.rebuildOnChange = false, 22 | this.onBrightnessChange, 23 | }) : super(); 24 | 25 | final bool rebuildOnChange; 26 | final BrightnessCallback? onBrightnessChange; 27 | 28 | @override 29 | _PlatformBrightnessState createState() => _PlatformBrightnessState(); 30 | } 31 | 32 | class _PlatformBrightnessState 33 | extends HookState 34 | with 35 | // ignore: prefer_mixin 36 | WidgetsBindingObserver { 37 | late Brightness _brightness; 38 | 39 | @override 40 | String? get debugLabel => 'usePlatformBrightness'; 41 | 42 | @override 43 | void initHook() { 44 | super.initHook(); 45 | _brightness = WidgetsBinding.instance.platformDispatcher.platformBrightness; 46 | WidgetsBinding.instance.addObserver(this); 47 | } 48 | 49 | @override 50 | Brightness build(BuildContext context) => _brightness; 51 | 52 | @override 53 | void dispose() { 54 | WidgetsBinding.instance.removeObserver(this); 55 | super.dispose(); 56 | } 57 | 58 | @override 59 | void didChangePlatformBrightness() { 60 | super.didChangePlatformBrightness(); 61 | final _previous = _brightness; 62 | _brightness = WidgetsBinding.instance.platformDispatcher.platformBrightness; 63 | hook.onBrightnessChange?.call(_previous, _brightness); 64 | 65 | if (hook.rebuildOnChange) { 66 | setState(() {}); 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /packages/flutter_hooks/lib/src/scroll_controller.dart: -------------------------------------------------------------------------------- 1 | part of 'hooks.dart'; 2 | 3 | /// Creates [ScrollController] that will be disposed automatically. 4 | /// 5 | /// See also: 6 | /// - [ScrollController] 7 | ScrollController useScrollController({ 8 | double initialScrollOffset = 0.0, 9 | bool keepScrollOffset = true, 10 | String? debugLabel, 11 | ScrollControllerCallback? onAttach, 12 | ScrollControllerCallback? onDetach, 13 | List? keys, 14 | }) { 15 | return use( 16 | _ScrollControllerHook( 17 | initialScrollOffset: initialScrollOffset, 18 | keepScrollOffset: keepScrollOffset, 19 | debugLabel: debugLabel, 20 | onAttach: onAttach, 21 | onDetach: onDetach, 22 | keys: keys, 23 | ), 24 | ); 25 | } 26 | 27 | class _ScrollControllerHook extends Hook { 28 | const _ScrollControllerHook({ 29 | required this.initialScrollOffset, 30 | required this.keepScrollOffset, 31 | this.debugLabel, 32 | this.onAttach, 33 | this.onDetach, 34 | List? keys, 35 | }) : super(keys: keys); 36 | 37 | final double initialScrollOffset; 38 | final bool keepScrollOffset; 39 | final String? debugLabel; 40 | final ScrollControllerCallback? onAttach; 41 | final ScrollControllerCallback? onDetach; 42 | 43 | @override 44 | HookState> createState() => 45 | _ScrollControllerHookState(); 46 | } 47 | 48 | class _ScrollControllerHookState 49 | extends HookState { 50 | late final controller = ScrollController( 51 | initialScrollOffset: hook.initialScrollOffset, 52 | keepScrollOffset: hook.keepScrollOffset, 53 | debugLabel: hook.debugLabel, 54 | onAttach: hook.onAttach, 55 | onDetach: hook.onDetach, 56 | ); 57 | 58 | @override 59 | ScrollController build(BuildContext context) => controller; 60 | 61 | @override 62 | void dispose() => controller.dispose(); 63 | 64 | @override 65 | String get debugLabel => 'useScrollController'; 66 | } 67 | -------------------------------------------------------------------------------- /packages/flutter_hooks/lib/src/search_controller.dart: -------------------------------------------------------------------------------- 1 | part of 'hooks.dart'; 2 | 3 | /// Creates a [SearchController] that will be disposed automatically. 4 | /// 5 | /// See also: 6 | /// - [SearchController] 7 | SearchController useSearchController({List? keys}) { 8 | return use(_SearchControllerHook(keys: keys)); 9 | } 10 | 11 | class _SearchControllerHook extends Hook { 12 | const _SearchControllerHook({List? keys}) : super(keys: keys); 13 | 14 | @override 15 | HookState> createState() => 16 | _SearchControllerHookState(); 17 | } 18 | 19 | class _SearchControllerHookState 20 | extends HookState { 21 | final controller = SearchController(); 22 | 23 | @override 24 | String get debugLabel => 'useSearchController'; 25 | 26 | @override 27 | SearchController build(BuildContext context) => controller; 28 | 29 | @override 30 | void dispose() => controller.dispose(); 31 | } 32 | -------------------------------------------------------------------------------- /packages/flutter_hooks/lib/src/tab_controller.dart: -------------------------------------------------------------------------------- 1 | part of 'hooks.dart'; 2 | 3 | /// Creates a [TabController] that will be disposed automatically. 4 | /// 5 | /// See also: 6 | /// - [TabController] 7 | TabController useTabController({ 8 | required int initialLength, 9 | Duration? animationDuration = kTabScrollDuration, 10 | TickerProvider? vsync, 11 | int initialIndex = 0, 12 | List? keys, 13 | }) { 14 | vsync ??= useSingleTickerProvider(keys: keys); 15 | 16 | return use( 17 | _TabControllerHook( 18 | vsync: vsync, 19 | length: initialLength, 20 | initialIndex: initialIndex, 21 | animationDuration: animationDuration, 22 | keys: keys, 23 | ), 24 | ); 25 | } 26 | 27 | class _TabControllerHook extends Hook { 28 | const _TabControllerHook({ 29 | required this.length, 30 | required this.vsync, 31 | required this.initialIndex, 32 | required this.animationDuration, 33 | super.keys, 34 | }); 35 | 36 | final int length; 37 | final TickerProvider vsync; 38 | final int initialIndex; 39 | final Duration? animationDuration; 40 | 41 | @override 42 | HookState> createState() => 43 | _TabControllerHookState(); 44 | } 45 | 46 | class _TabControllerHookState 47 | extends HookState { 48 | late final controller = TabController( 49 | length: hook.length, 50 | initialIndex: hook.initialIndex, 51 | animationDuration: hook.animationDuration, 52 | vsync: hook.vsync, 53 | ); 54 | 55 | @override 56 | TabController build(BuildContext context) => controller; 57 | 58 | @override 59 | void dispose() => controller.dispose(); 60 | 61 | @override 62 | String get debugLabel => 'useTabController'; 63 | } 64 | -------------------------------------------------------------------------------- /packages/flutter_hooks/lib/src/text_controller.dart: -------------------------------------------------------------------------------- 1 | part of 'hooks.dart'; 2 | 3 | class _TextEditingControllerHookCreator { 4 | const _TextEditingControllerHookCreator(); 5 | 6 | /// Creates a [TextEditingController] that will be disposed automatically. 7 | /// 8 | /// The [text] parameter can be used to set the initial value of the 9 | /// controller. 10 | TextEditingController call({String? text, List? keys}) { 11 | return use(_TextEditingControllerHook(text, keys)); 12 | } 13 | 14 | /// Creates a [TextEditingController] from the initial [value] that will 15 | /// be disposed automatically. 16 | TextEditingController fromValue( 17 | TextEditingValue value, [ 18 | List? keys, 19 | ]) { 20 | return use(_TextEditingControllerHook.fromValue(value, keys)); 21 | } 22 | } 23 | 24 | /// Creates a [TextEditingController], either via an initial text or an initial 25 | /// [TextEditingValue]. 26 | /// 27 | /// To use a [TextEditingController] with an optional initial text, use: 28 | /// ```dart 29 | /// final controller = useTextEditingController(text: 'initial text'); 30 | /// ``` 31 | /// 32 | /// To use a [TextEditingController] with an optional initial value, use: 33 | /// ```dart 34 | /// final controller = useTextEditingController 35 | /// .fromValue(TextEditingValue.empty); 36 | /// ``` 37 | /// 38 | /// Changing the text or initial value after the widget has been built has no 39 | /// effect whatsoever. To update the value in a callback, for instance after a 40 | /// button was pressed, use the [TextEditingController.text] or 41 | /// [TextEditingController.value] setters. To have the [TextEditingController] 42 | /// reflect changing values, you can use [useEffect]. This example will update 43 | /// the [TextEditingController.text] whenever a provided [ValueListenable] 44 | /// changes: 45 | /// ```dart 46 | /// final controller = useTextEditingController(); 47 | /// final update = useValueListenable(myTextControllerUpdates); 48 | /// 49 | /// useEffect(() { 50 | /// controller.text = update; 51 | /// }, [update]); 52 | /// ``` 53 | /// 54 | /// See also: 55 | /// - [TextEditingController], which this hook creates. 56 | const useTextEditingController = _TextEditingControllerHookCreator(); 57 | 58 | class _TextEditingControllerHook extends Hook { 59 | const _TextEditingControllerHook( 60 | this.initialText, [ 61 | List? keys, 62 | ]) : initialValue = null, 63 | super(keys: keys); 64 | 65 | const _TextEditingControllerHook.fromValue( 66 | TextEditingValue this.initialValue, [ 67 | List? keys, 68 | ]) : initialText = null, 69 | super(keys: keys); 70 | 71 | final String? initialText; 72 | final TextEditingValue? initialValue; 73 | 74 | @override 75 | _TextEditingControllerHookState createState() { 76 | return _TextEditingControllerHookState(); 77 | } 78 | } 79 | 80 | class _TextEditingControllerHookState 81 | extends HookState { 82 | late final _controller = hook.initialValue != null 83 | ? TextEditingController.fromValue(hook.initialValue) 84 | : TextEditingController(text: hook.initialText); 85 | 86 | @override 87 | TextEditingController build(BuildContext context) => _controller; 88 | 89 | @override 90 | void dispose() => _controller.dispose(); 91 | 92 | @override 93 | String get debugLabel => 'useTextEditingController'; 94 | } 95 | -------------------------------------------------------------------------------- /packages/flutter_hooks/lib/src/transformation_controller.dart: -------------------------------------------------------------------------------- 1 | part of 'hooks.dart'; 2 | 3 | /// Creates and disposes a [TransformationController]. 4 | /// 5 | /// See also: 6 | /// - [TransformationController] 7 | TransformationController useTransformationController({ 8 | Matrix4? initialValue, 9 | List? keys, 10 | }) { 11 | return use( 12 | _TransformationControllerHook( 13 | initialValue: initialValue, 14 | keys: keys, 15 | ), 16 | ); 17 | } 18 | 19 | class _TransformationControllerHook extends Hook { 20 | const _TransformationControllerHook({ 21 | required this.initialValue, 22 | List? keys, 23 | }) : super(keys: keys); 24 | 25 | final Matrix4? initialValue; 26 | 27 | @override 28 | HookState> 29 | createState() => _TransformationControllerHookState(); 30 | } 31 | 32 | class _TransformationControllerHookState 33 | extends HookState { 34 | late final controller = TransformationController(hook.initialValue); 35 | 36 | @override 37 | TransformationController build(BuildContext context) => controller; 38 | 39 | @override 40 | void dispose() => controller.dispose(); 41 | 42 | @override 43 | String get debugLabel => 'useTransformationController'; 44 | } 45 | -------------------------------------------------------------------------------- /packages/flutter_hooks/lib/src/tree_sliver_controller.dart: -------------------------------------------------------------------------------- 1 | part of 'hooks.dart'; 2 | 3 | /// Creates a [TreeSliverController] that will be disposed automatically. 4 | /// 5 | /// See also: 6 | /// - [TreeSliverController] 7 | TreeSliverController useTreeSliverController() { 8 | return use(const _TreeSliverControllerHook()); 9 | } 10 | 11 | class _TreeSliverControllerHook extends Hook { 12 | const _TreeSliverControllerHook(); 13 | 14 | @override 15 | HookState> createState() => 16 | _TreeSliverControllerHookState(); 17 | } 18 | 19 | class _TreeSliverControllerHookState 20 | extends HookState { 21 | final controller = TreeSliverController(); 22 | 23 | @override 24 | String get debugLabel => 'useTreeSliverController'; 25 | 26 | @override 27 | TreeSliverController build(BuildContext context) => controller; 28 | } 29 | -------------------------------------------------------------------------------- /packages/flutter_hooks/lib/src/widget_states_controller.dart: -------------------------------------------------------------------------------- 1 | part of 'hooks.dart'; 2 | 3 | /// Creates a [WidgetStatesController] that will be disposed automatically. 4 | /// 5 | /// See also: 6 | /// - [WidgetStatesController] 7 | WidgetStatesController useWidgetStatesController({ 8 | Set? values, 9 | List? keys, 10 | }) { 11 | return use( 12 | _WidgetStatesControllerHook( 13 | values: values, 14 | keys: keys, 15 | ), 16 | ); 17 | } 18 | 19 | class _WidgetStatesControllerHook extends Hook { 20 | const _WidgetStatesControllerHook({ 21 | required this.values, 22 | super.keys, 23 | }); 24 | 25 | final Set? values; 26 | 27 | @override 28 | HookState> 29 | createState() => _WidgetStateControllerHookState(); 30 | } 31 | 32 | class _WidgetStateControllerHookState 33 | extends HookState { 34 | late final controller = WidgetStatesController(hook.values); 35 | 36 | @override 37 | WidgetStatesController build(BuildContext context) => controller; 38 | 39 | @override 40 | void dispose() => controller.dispose(); 41 | 42 | @override 43 | String get debugLabel => 'useWidgetStatesController'; 44 | } 45 | -------------------------------------------------------------------------------- /packages/flutter_hooks/lib/src/widgets_binding_observer.dart: -------------------------------------------------------------------------------- 1 | part of 'hooks.dart'; 2 | 3 | /// A callback triggered when the app life cycle changes. 4 | typedef LifecycleCallback = FutureOr Function( 5 | AppLifecycleState? previous, 6 | AppLifecycleState current, 7 | ); 8 | 9 | /// Returns the current [AppLifecycleState] value and rebuilds the widget when it changes. 10 | AppLifecycleState? useAppLifecycleState() { 11 | return use(const _AppLifecycleHook(rebuildOnChange: true)); 12 | } 13 | 14 | /// Listens to the [AppLifecycleState]. 15 | void useOnAppLifecycleStateChange(LifecycleCallback? onStateChanged) { 16 | use(_AppLifecycleHook(onStateChanged: onStateChanged)); 17 | } 18 | 19 | class _AppLifecycleHook extends Hook { 20 | const _AppLifecycleHook({ 21 | this.rebuildOnChange = false, 22 | this.onStateChanged, 23 | }) : super(); 24 | 25 | final bool rebuildOnChange; 26 | final LifecycleCallback? onStateChanged; 27 | 28 | @override 29 | __AppLifecycleStateState createState() => __AppLifecycleStateState(); 30 | } 31 | 32 | class __AppLifecycleStateState 33 | extends HookState 34 | with 35 | // ignore: prefer_mixin 36 | WidgetsBindingObserver { 37 | AppLifecycleState? _state; 38 | 39 | @override 40 | void initHook() { 41 | super.initHook(); 42 | _state = WidgetsBinding.instance.lifecycleState; 43 | WidgetsBinding.instance.addObserver(this); 44 | } 45 | 46 | @override 47 | AppLifecycleState? build(BuildContext context) => _state; 48 | 49 | @override 50 | void dispose() { 51 | super.dispose(); 52 | WidgetsBinding.instance.removeObserver(this); 53 | } 54 | 55 | @override 56 | void didChangeAppLifecycleState(AppLifecycleState state) { 57 | final previous = _state; 58 | _state = state; 59 | hook.onStateChanged?.call(previous, state); 60 | 61 | if (hook.rebuildOnChange) { 62 | setState(() {}); 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /packages/flutter_hooks/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: flutter_hooks 2 | description: A flutter implementation of React hooks. It adds a new kind of widget with enhanced code reuse. 3 | homepage: https://github.com/rrousselGit/flutter_hooks 4 | repository: https://github.com/rrousselGit/flutter_hooks/tree/master/packages/flutter_hooks 5 | issue_tracker: https://github.com/rrousselGit/flutter_hooks/issues 6 | version: 0.21.2 7 | 8 | environment: 9 | sdk: ">=2.17.0 <3.0.0" 10 | flutter: ">=3.32.0" 11 | 12 | dependencies: 13 | flutter: 14 | sdk: flutter 15 | 16 | dev_dependencies: 17 | flutter_test: 18 | sdk: flutter 19 | mockito: ^5.0.16 20 | -------------------------------------------------------------------------------- /packages/flutter_hooks/test/carousel_controller_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/foundation.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter_hooks/src/framework.dart'; 4 | import 'package:flutter_hooks/src/hooks.dart'; 5 | import 'package:flutter_test/flutter_test.dart'; 6 | 7 | import 'mock.dart'; 8 | 9 | void main() { 10 | testWidgets('debugFillProperties', (tester) async { 11 | await tester.pumpWidget( 12 | HookBuilder(builder: (context) { 13 | useCarouselController(); 14 | return const SizedBox(); 15 | }), 16 | ); 17 | 18 | final element = tester.element(find.byType(HookBuilder)); 19 | 20 | expect( 21 | element 22 | .toDiagnosticsNode(style: DiagnosticsTreeStyle.offstage) 23 | .toStringDeep(), 24 | equalsIgnoringHashCodes( 25 | 'HookBuilder\n' 26 | ' │ useCarouselController: CarouselController#00000(no clients)\n' 27 | ' └SizedBox(renderObject: RenderConstrainedBox#00000)\n', 28 | ), 29 | ); 30 | }); 31 | 32 | group('useCarouselController', () { 33 | testWidgets('initial values matches with real constructor', (tester) async { 34 | late CarouselController controller; 35 | late CarouselController controller2; 36 | 37 | await tester.pumpWidget( 38 | HookBuilder(builder: (context) { 39 | controller2 = CarouselController(); 40 | controller = useCarouselController(); 41 | return Container(); 42 | }), 43 | ); 44 | 45 | expect(controller.initialItem, controller2.initialItem); 46 | expect(controller.initialScrollOffset, controller2.initialScrollOffset); 47 | expect(controller.keepScrollOffset, controller2.keepScrollOffset); 48 | expect(controller.onAttach, controller2.onAttach); 49 | expect(controller.onDetach, controller2.onDetach); 50 | }); 51 | 52 | testWidgets("returns a CarouselController that doesn't change", 53 | (tester) async { 54 | late CarouselController controller; 55 | late CarouselController controller2; 56 | 57 | await tester.pumpWidget( 58 | HookBuilder(builder: (context) { 59 | controller = useCarouselController(); 60 | return Container(); 61 | }), 62 | ); 63 | 64 | expect(controller, isA()); 65 | 66 | await tester.pumpWidget( 67 | HookBuilder(builder: (context) { 68 | controller2 = useCarouselController(); 69 | return Container(); 70 | }), 71 | ); 72 | 73 | expect(identical(controller, controller2), isTrue); 74 | }); 75 | 76 | testWidgets('passes hook parameters to the CarouselController', 77 | (tester) async { 78 | late CarouselController controller; 79 | 80 | await tester.pumpWidget( 81 | HookBuilder( 82 | builder: (context) { 83 | controller = useCarouselController( 84 | initialItem: 42, 85 | ); 86 | 87 | return Container(); 88 | }, 89 | ), 90 | ); 91 | 92 | expect(controller.initialItem, 42); 93 | }); 94 | 95 | testWidgets('disposes the CarouselController on unmount', (tester) async { 96 | late CarouselController controller; 97 | 98 | await tester.pumpWidget( 99 | HookBuilder( 100 | builder: (context) { 101 | controller = useCarouselController(); 102 | return Container(); 103 | }, 104 | ), 105 | ); 106 | 107 | // pump another widget so that the old one gets disposed 108 | await tester.pumpWidget(Container()); 109 | 110 | expect( 111 | () => controller.addListener(() {}), 112 | throwsA(isFlutterError.having( 113 | (e) => e.message, 'message', contains('disposed'))), 114 | ); 115 | }); 116 | }); 117 | } 118 | -------------------------------------------------------------------------------- /packages/flutter_hooks/test/hook_builder_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/widgets.dart'; 2 | import 'package:flutter_hooks/flutter_hooks.dart'; 3 | import 'package:flutter_test/flutter_test.dart'; 4 | 5 | void main() { 6 | testWidgets('simple build', (tester) async { 7 | await tester.pumpWidget( 8 | HookBuilder(builder: (context) { 9 | final state = useState(42).value; 10 | return Text('$state', textDirection: TextDirection.ltr); 11 | }), 12 | ); 13 | 14 | expect(find.text('42'), findsOneWidget); 15 | }); 16 | } 17 | -------------------------------------------------------------------------------- /packages/flutter_hooks/test/mock.dart: -------------------------------------------------------------------------------- 1 | // ignore_for_file: one_member_abstracts 2 | 3 | import 'package:flutter/widgets.dart'; 4 | import 'package:flutter_hooks/flutter_hooks.dart'; 5 | import 'package:flutter_test/flutter_test.dart'; 6 | import 'package:mockito/mockito.dart'; 7 | 8 | export 'package:flutter_test/flutter_test.dart' 9 | hide 10 | Func0, 11 | Func1, 12 | Func2, 13 | Func3, 14 | Func4, 15 | Func5, 16 | Func6, 17 | // ignore: undefined_hidden_name, Fake is only available in master 18 | Fake; 19 | export 'package:mockito/mockito.dart'; 20 | 21 | class HookTest extends Hook { 22 | // ignore: prefer_const_constructors_in_immutables 23 | HookTest({ 24 | this.build, 25 | this.dispose, 26 | this.initHook, 27 | this.didUpdateHook, 28 | this.reassemble, 29 | this.createStateFn, 30 | this.didBuild, 31 | this.deactivate, 32 | List? keys, 33 | }) : super(keys: keys); 34 | 35 | final R Function(BuildContext context)? build; 36 | final void Function()? dispose; 37 | final void Function()? didBuild; 38 | final void Function()? initHook; 39 | final void Function()? deactivate; 40 | final void Function(HookTest previousHook)? didUpdateHook; 41 | final void Function()? reassemble; 42 | final HookStateTest Function()? createStateFn; 43 | 44 | @override 45 | HookStateTest createState() => 46 | createStateFn != null ? createStateFn!() : HookStateTest(); 47 | } 48 | 49 | class HookStateTest extends HookState> { 50 | @override 51 | void initHook() { 52 | super.initHook(); 53 | hook.initHook?.call(); 54 | } 55 | 56 | @override 57 | void dispose() { 58 | hook.dispose?.call(); 59 | } 60 | 61 | @override 62 | void didUpdateHook(HookTest oldHook) { 63 | super.didUpdateHook(oldHook); 64 | hook.didUpdateHook?.call(oldHook); 65 | } 66 | 67 | @override 68 | void reassemble() { 69 | super.reassemble(); 70 | hook.reassemble?.call(); 71 | } 72 | 73 | @override 74 | void deactivate() { 75 | super.deactivate(); 76 | hook.deactivate?.call(); 77 | } 78 | 79 | @override 80 | R? build(BuildContext context) { 81 | if (hook.build != null) { 82 | return hook.build!(context); 83 | } 84 | return null; 85 | } 86 | } 87 | 88 | Element _rootOf(Element element) { 89 | late Element root; 90 | element.visitAncestorElements((e) { 91 | root = e; 92 | return true; 93 | }); 94 | return root; 95 | } 96 | 97 | void hotReload(WidgetTester tester) { 98 | final root = _rootOf(tester.allElements.first); 99 | 100 | TestWidgetsFlutterBinding.ensureInitialized().buildOwner?.reassemble(root); 101 | } 102 | 103 | class MockSetState extends Mock { 104 | void call(); 105 | } 106 | 107 | class MockInitHook extends Mock { 108 | void call(); 109 | } 110 | 111 | class MockCreateState>> 112 | extends Mock { 113 | MockCreateState(this.value); 114 | final T value; 115 | 116 | T call() { 117 | return super.noSuchMethod( 118 | Invocation.method(#call, []), 119 | returnValue: value, 120 | returnValueForMissingStub: value, 121 | ) as T; 122 | } 123 | } 124 | 125 | class MockBuild extends Mock { 126 | T call(BuildContext? context); 127 | } 128 | 129 | class MockDeactivate extends Mock { 130 | void call(); 131 | } 132 | 133 | class MockFlutterErrorDetails extends Mock implements FlutterErrorDetails { 134 | @override 135 | String toString({DiagnosticLevel? minLevel}) => super.toString(); 136 | } 137 | 138 | class MockErrorBuilder extends Mock { 139 | Widget call(FlutterErrorDetails error) => super.noSuchMethod( 140 | Invocation.getter(#call), 141 | returnValue: Container(), 142 | ) as Widget; 143 | } 144 | 145 | class MockOnError extends Mock { 146 | void call(FlutterErrorDetails? error); 147 | } 148 | 149 | class MockReassemble extends Mock { 150 | void call(); 151 | } 152 | 153 | class MockDidUpdateHook extends Mock { 154 | void call(HookTest? hook); 155 | } 156 | 157 | class MockDispose extends Mock { 158 | void call(); 159 | } 160 | -------------------------------------------------------------------------------- /packages/flutter_hooks/test/use_animation_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/foundation.dart'; 2 | import 'package:flutter/widgets.dart'; 3 | import 'package:flutter_hooks/flutter_hooks.dart'; 4 | 5 | import 'mock.dart'; 6 | 7 | void main() { 8 | testWidgets('debugFillProperties', (tester) async { 9 | await tester.pumpWidget( 10 | HookBuilder(builder: (context) { 11 | useAnimation(const AlwaysStoppedAnimation(42)); 12 | return const SizedBox(); 13 | }), 14 | ); 15 | 16 | final element = tester.element(find.byType(HookBuilder)); 17 | 18 | expect( 19 | element 20 | .toDiagnosticsNode(style: DiagnosticsTreeStyle.offstage) 21 | .toStringDeep(), 22 | equalsIgnoringHashCodes( 23 | 'HookBuilder\n' 24 | ' │ useAnimation: 42\n' 25 | ' └SizedBox(renderObject: RenderConstrainedBox#00000)\n', 26 | ), 27 | ); 28 | }); 29 | 30 | testWidgets('useAnimation', (tester) async { 31 | var listenable = AnimationController(vsync: tester); 32 | late double result; 33 | 34 | Future pump() { 35 | return tester.pumpWidget(HookBuilder( 36 | builder: (context) { 37 | result = useAnimation(listenable); 38 | return Container(); 39 | }, 40 | )); 41 | } 42 | 43 | await pump(); 44 | 45 | final element = tester.firstElement(find.byType(HookBuilder)); 46 | 47 | expect(result, 0); 48 | expect(element.dirty, false); 49 | listenable.value++; 50 | expect(element.dirty, true); 51 | await tester.pump(); 52 | expect(result, 1); 53 | expect(element.dirty, false); 54 | 55 | final previousListenable = listenable; 56 | listenable = AnimationController(vsync: tester); 57 | 58 | await pump(); 59 | 60 | expect(result, 0); 61 | expect(element.dirty, false); 62 | previousListenable.value++; 63 | expect(element.dirty, false); 64 | listenable.value++; 65 | expect(element.dirty, true); 66 | await tester.pump(); 67 | expect(result, 1); 68 | expect(element.dirty, false); 69 | 70 | await tester.pumpWidget(const SizedBox()); 71 | 72 | listenable.dispose(); 73 | previousListenable.dispose(); 74 | }); 75 | } 76 | -------------------------------------------------------------------------------- /packages/flutter_hooks/test/use_app_lifecycle_state_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/widgets.dart'; 2 | import 'package:flutter_hooks/flutter_hooks.dart'; 3 | 4 | import 'mock.dart'; 5 | 6 | void main() { 7 | group('useAppLifecycleState', () { 8 | testWidgets('returns initial value and rebuild widgets on change', 9 | (tester) async { 10 | tester.binding.handleAppLifecycleStateChanged(AppLifecycleState.resumed); 11 | 12 | await tester.pumpWidget( 13 | HookBuilder( 14 | builder: (context) { 15 | final state = useAppLifecycleState(); 16 | return Text('$state', textDirection: TextDirection.ltr); 17 | }, 18 | ), 19 | ); 20 | 21 | expect(find.text('AppLifecycleState.resumed'), findsOneWidget); 22 | 23 | tester.binding.handleAppLifecycleStateChanged(AppLifecycleState.inactive); 24 | await tester.pump(); 25 | 26 | expect(find.text('AppLifecycleState.inactive'), findsOneWidget); 27 | }); 28 | }); 29 | 30 | group('useOnAppLifecycleStateChange', () { 31 | testWidgets( 32 | 'sends previous and new value on change, without rebuilding widgets', 33 | (tester) async { 34 | tester.binding.handleAppLifecycleStateChanged(AppLifecycleState.resumed); 35 | var buildCount = 0; 36 | final listener = AppLifecycleStateListener(); 37 | 38 | await tester.pumpWidget( 39 | HookBuilder( 40 | builder: (context) { 41 | buildCount++; 42 | useOnAppLifecycleStateChange(listener); 43 | return Container(); 44 | }, 45 | ), 46 | ); 47 | 48 | expect(buildCount, 1); 49 | verifyZeroInteractions(listener); 50 | 51 | tester.binding.handleAppLifecycleStateChanged(AppLifecycleState.paused); 52 | await tester.pump(); 53 | 54 | expect(buildCount, 1); 55 | verify(listener(AppLifecycleState.resumed, AppLifecycleState.paused)); 56 | verifyNoMoreInteractions(listener); 57 | 58 | tester.binding.handleAppLifecycleStateChanged(AppLifecycleState.resumed); 59 | await tester.pump(); 60 | 61 | expect(buildCount, 1); 62 | verify(listener(AppLifecycleState.paused, AppLifecycleState.resumed)); 63 | verifyNoMoreInteractions(listener); 64 | }); 65 | }); 66 | } 67 | 68 | class AppLifecycleStateListener extends Mock { 69 | void call(AppLifecycleState? prev, AppLifecycleState state); 70 | } 71 | -------------------------------------------------------------------------------- /packages/flutter_hooks/test/use_automatic_keep_alive_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/foundation.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter_hooks/src/framework.dart'; 4 | import 'package:flutter_hooks/src/hooks.dart'; 5 | 6 | import 'mock.dart'; 7 | 8 | void main() { 9 | group('useAutomaticKeepAlive', () { 10 | testWidgets('debugFillProperties', (tester) async { 11 | await tester.pumpWidget( 12 | HookBuilder(builder: (context) { 13 | useAutomaticKeepAlive(); 14 | return const SizedBox(); 15 | }), 16 | ); 17 | 18 | await tester.pump(); 19 | 20 | final element = tester.element(find.byType(HookBuilder)); 21 | 22 | expect( 23 | element 24 | .toDiagnosticsNode(style: DiagnosticsTreeStyle.offstage) 25 | .toStringDeep(), 26 | equalsIgnoringHashCodes( 27 | 'HookBuilder\n' 28 | " │ useAutomaticKeepAlive: Instance of 'KeepAliveHandle'\n" 29 | ' └SizedBox(renderObject: RenderConstrainedBox#00000)\n', 30 | ), 31 | ); 32 | }); 33 | 34 | testWidgets('keeps widget alive in a TabView', (tester) async { 35 | await tester.pumpWidget( 36 | Directionality( 37 | textDirection: TextDirection.ltr, 38 | child: DefaultTabController( 39 | length: 2, 40 | child: TabBarView( 41 | children: [ 42 | HookBuilder(builder: (context) { 43 | useAutomaticKeepAlive(); 44 | return Container(); 45 | }), 46 | Container(), 47 | ], 48 | ), 49 | ), 50 | ), 51 | ); 52 | await tester.pump(); 53 | 54 | final findKeepAlive = find.byType(AutomaticKeepAlive); 55 | final keepAlive = tester.element(findKeepAlive); 56 | 57 | expect(findKeepAlive, findsOneWidget); 58 | expect( 59 | keepAlive 60 | .toDiagnosticsNode(style: DiagnosticsTreeStyle.shallow) 61 | .toStringDeep(), 62 | equalsIgnoringHashCodes( 63 | 'AutomaticKeepAlive:\n' 64 | ' state: _AutomaticKeepAliveState#00000(keeping subtree alive,\n' 65 | ' handles: 1 active client)\n', 66 | ), 67 | ); 68 | }); 69 | 70 | testWidgets( 71 | 'start keep alive when wantKeepAlive changes to true', 72 | (tester) async { 73 | final keepAliveNotifier = ValueNotifier(false); 74 | 75 | await tester.pumpWidget( 76 | Directionality( 77 | textDirection: TextDirection.ltr, 78 | child: DefaultTabController( 79 | length: 2, 80 | child: TabBarView( 81 | children: [ 82 | HookBuilder(builder: (context) { 83 | final wantKeepAlive = useValueListenable(keepAliveNotifier); 84 | useAutomaticKeepAlive(wantKeepAlive: wantKeepAlive); 85 | return Container(); 86 | }), 87 | Container(), 88 | ], 89 | ), 90 | ), 91 | ), 92 | ); 93 | await tester.pump(); 94 | 95 | final findKeepAlive = find.byType(AutomaticKeepAlive); 96 | final keepAlive = tester.element(findKeepAlive); 97 | 98 | expect(findKeepAlive, findsOneWidget); 99 | expect( 100 | keepAlive 101 | .toDiagnosticsNode(style: DiagnosticsTreeStyle.shallow) 102 | .toStringDeep(), 103 | equalsIgnoringHashCodes( 104 | 'AutomaticKeepAlive:\n' 105 | ' state: _AutomaticKeepAliveState#00000(handles: no notifications\n' 106 | ' ever received)\n', 107 | ), 108 | ); 109 | 110 | keepAliveNotifier.value = true; 111 | await tester.pump(); 112 | 113 | expect(findKeepAlive, findsOneWidget); 114 | expect( 115 | keepAlive 116 | .toDiagnosticsNode(style: DiagnosticsTreeStyle.shallow) 117 | .toStringDeep(), 118 | equalsIgnoringHashCodes( 119 | 'AutomaticKeepAlive:\n' 120 | ' state: _AutomaticKeepAliveState#00000(keeping subtree alive,\n' 121 | ' handles: 1 active client)\n', 122 | ), 123 | ); 124 | }, 125 | ); 126 | }); 127 | } 128 | -------------------------------------------------------------------------------- /packages/flutter_hooks/test/use_callback_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/widgets.dart'; 2 | import 'package:flutter_hooks/flutter_hooks.dart'; 3 | 4 | import 'mock.dart'; 5 | 6 | void main() { 7 | testWidgets('useCallback', (tester) async { 8 | late int Function() fn; 9 | 10 | await tester.pumpWidget( 11 | HookBuilder(builder: (context) { 12 | fn = useCallback(() => 42, []); 13 | return Container(); 14 | }), 15 | ); 16 | 17 | expect(fn(), 42); 18 | 19 | late int Function() fn2; 20 | 21 | await tester.pumpWidget( 22 | HookBuilder(builder: (context) { 23 | fn2 = useCallback(() => 42, []); 24 | return Container(); 25 | }), 26 | ); 27 | 28 | expect(fn2, fn); 29 | 30 | late int Function() fn3; 31 | 32 | await tester.pumpWidget( 33 | HookBuilder(builder: (context) { 34 | fn3 = useCallback(() => 21, [42]); 35 | return Container(); 36 | }), 37 | ); 38 | 39 | expect(fn3, isNot(fn)); 40 | expect(fn3(), 21); 41 | }); 42 | 43 | testWidgets( 44 | 'should return same function when keys are not specified', 45 | (tester) async { 46 | late Function fn1; 47 | late Function fn2; 48 | 49 | await tester.pumpWidget( 50 | HookBuilder( 51 | key: const Key('hook_builder'), 52 | builder: (context) { 53 | fn1 = useCallback(() {}); 54 | return Container(); 55 | }, 56 | ), 57 | ); 58 | 59 | await tester.pumpWidget( 60 | HookBuilder( 61 | key: const Key('hook_builder'), 62 | builder: (context) { 63 | fn2 = useCallback(() {}); 64 | return Container(); 65 | }, 66 | ), 67 | ); 68 | 69 | expect(fn1, same(fn2)); 70 | }, 71 | ); 72 | } 73 | -------------------------------------------------------------------------------- /packages/flutter_hooks/test/use_context_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/widgets.dart'; 2 | import 'package:flutter_hooks/flutter_hooks.dart'; 3 | import 'package:flutter_test/flutter_test.dart'; 4 | 5 | import 'mock.dart'; 6 | 7 | void main() { 8 | group('useContext', () { 9 | testWidgets('returns current BuildContext during build', (tester) async { 10 | late BuildContext res; 11 | 12 | await tester.pumpWidget(HookBuilder(builder: (context) { 13 | res = useContext(); 14 | return Container(); 15 | })); 16 | 17 | final context = tester.firstElement(find.byType(HookBuilder)); 18 | 19 | expect(res, context); 20 | }); 21 | 22 | testWidgets('crashed outside of build', (tester) async { 23 | expect(useContext, throwsAssertionError); 24 | await tester.pumpWidget(HookBuilder( 25 | builder: (context) { 26 | useContext(); 27 | return Container(); 28 | }, 29 | )); 30 | expect(useContext, throwsAssertionError); 31 | }); 32 | }); 33 | } 34 | -------------------------------------------------------------------------------- /packages/flutter_hooks/test/use_debounce_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/foundation.dart'; 2 | import 'package:flutter/widgets.dart'; 3 | import 'package:flutter_hooks/flutter_hooks.dart'; 4 | import 'package:flutter_test/flutter_test.dart'; 5 | 6 | void main() { 7 | testWidgets('debugFillProperties', (tester) async { 8 | await tester.pumpWidget( 9 | HookBuilder(builder: (context) { 10 | useDebounced(42, const Duration(milliseconds: 500)); 11 | return const SizedBox(); 12 | }), 13 | ); 14 | 15 | // await the debouncer timeout. 16 | await tester.pumpAndSettle(const Duration(milliseconds: 500)); 17 | 18 | final element = tester.element(find.byType(HookBuilder)); 19 | 20 | expect( 21 | element 22 | .toDiagnosticsNode(style: DiagnosticsTreeStyle.offstage) 23 | .toStringDeep(), 24 | equalsIgnoringHashCodes( 25 | 'HookBuilder\n' 26 | ' │ useDebounced: 42\n' 27 | ' └SizedBox(renderObject: RenderConstrainedBox#00000)\n', 28 | ), 29 | ); 30 | }); 31 | 32 | group('useDebounced', () { 33 | testWidgets('default value is null', (tester) async { 34 | await tester.pumpWidget( 35 | HookBuilder(builder: (context) { 36 | final debounced = useDebounced( 37 | 'test', 38 | const Duration(milliseconds: 500), 39 | ); 40 | return Text( 41 | debounced.toString(), 42 | textDirection: TextDirection.ltr, 43 | ); 44 | }), 45 | ); 46 | expect(find.text('null'), findsOneWidget); 47 | }); 48 | testWidgets('basic', (tester) async { 49 | await tester.pumpWidget( 50 | HookBuilder( 51 | builder: (context) { 52 | final textValueNotifier = useState('Hello'); 53 | final debounced = useDebounced( 54 | textValueNotifier.value, 55 | const Duration(milliseconds: 500), 56 | ); 57 | 58 | useEffect(() { 59 | textValueNotifier.value = 'World'; 60 | return null; 61 | }, [textValueNotifier.value]); 62 | 63 | return Text( 64 | debounced.toString(), 65 | textDirection: TextDirection.ltr, 66 | ); 67 | }, 68 | ), 69 | ); 70 | 71 | // Ensure that the initial value displayed is 'null' 72 | expect(find.text('null'), findsOneWidget); 73 | 74 | // Ensure that after a 500ms delay, the value 'Hello' of 'textValueNotifier' 75 | // is reflected in 'debounced' and displayed 76 | await tester.pumpAndSettle(const Duration(milliseconds: 500)); 77 | expect(find.text('Hello'), findsOneWidget); 78 | 79 | // Ensure that after another 500ms delay, the value 'World' assigned to 80 | // 'textValueNotifier' in the useEffect is reflected in 'debounced' 81 | // and displayed 82 | await tester.pumpAndSettle(const Duration(milliseconds: 500)); 83 | expect(find.text('World'), findsOneWidget); 84 | }); 85 | }); 86 | } 87 | -------------------------------------------------------------------------------- /packages/flutter_hooks/test/use_draggable_scrollable_controller_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/foundation.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter_hooks/src/framework.dart'; 4 | import 'package:flutter_hooks/src/hooks.dart'; 5 | 6 | import 'mock.dart'; 7 | 8 | void main() { 9 | testWidgets('debugFillProperties', (tester) async { 10 | await tester.pumpWidget( 11 | HookBuilder(builder: (context) { 12 | useDraggableScrollableController(); 13 | return const SizedBox(); 14 | }), 15 | ); 16 | 17 | await tester.pump(); 18 | 19 | final element = tester.element(find.byType(HookBuilder)); 20 | 21 | expect( 22 | element 23 | .toDiagnosticsNode(style: DiagnosticsTreeStyle.offstage) 24 | .toStringDeep(), 25 | equalsIgnoringHashCodes('HookBuilder\n' 26 | ' │ useDraggableScrollableController: Instance of\n' 27 | " │ 'DraggableScrollableController'\n" 28 | ' └SizedBox(renderObject: RenderConstrainedBox#00000)\n'), 29 | ); 30 | }); 31 | 32 | group('useDraggableScrollableController', () { 33 | testWidgets( 34 | 'controller functions correctly and initial values matches with real constructor', 35 | (tester) async { 36 | late DraggableScrollableController controller; 37 | final controller2 = DraggableScrollableController(); 38 | 39 | await tester.pumpWidget(MaterialApp( 40 | home: Scaffold( 41 | body: HookBuilder(builder: (context) { 42 | return Column( 43 | children: [ 44 | ElevatedButton( 45 | onPressed: () { 46 | showBottomSheet( 47 | context: context, 48 | builder: (context) { 49 | // Using a builder here to ensure that the controller is 50 | // disposed when the sheet is closed. 51 | return HookBuilder(builder: (context) { 52 | controller = useDraggableScrollableController(); 53 | return DraggableScrollableSheet( 54 | controller: controller, 55 | builder: (context, scrollController) { 56 | return ListView.builder( 57 | controller: scrollController, 58 | itemCount: 100, 59 | itemBuilder: (context, index) { 60 | return ListTile( 61 | title: Text('Item $index on Sheet 1'), 62 | ); 63 | }, 64 | ); 65 | }, 66 | ); 67 | }); 68 | }); 69 | }, 70 | child: const Text('Open Sheet 1'), 71 | ), 72 | ElevatedButton( 73 | onPressed: () { 74 | showBottomSheet( 75 | context: context, 76 | builder: (context) { 77 | return DraggableScrollableSheet( 78 | controller: controller2, 79 | builder: (context, scrollController) { 80 | return ListView.builder( 81 | controller: scrollController, 82 | itemCount: 100, 83 | itemBuilder: (context, index) { 84 | return ListTile( 85 | title: Text('Item $index on Sheet 2'), 86 | ); 87 | }, 88 | ); 89 | }, 90 | ); 91 | }, 92 | ); 93 | }, 94 | child: const Text('Open Sheet 2'), 95 | ) 96 | ], 97 | ); 98 | }), 99 | ), 100 | )); 101 | 102 | // Open Sheet 1 and get the initial values 103 | await tester.tap(find.text('Open Sheet 1')); 104 | await tester.pumpAndSettle(); 105 | final controllerPixels = controller.pixels; 106 | final controllerSize = controller.size; 107 | final controllerIsAttached = controller.isAttached; 108 | // Close Sheet 1 by dragging it down 109 | await tester.fling( 110 | find.byType(DraggableScrollableSheet), const Offset(0, 500), 300); 111 | await tester.pumpAndSettle(); 112 | 113 | // Open Sheet 2 and get the initial values 114 | await tester.tap(find.text('Open Sheet 2')); 115 | await tester.pumpAndSettle(); 116 | final controller2Pixels = controller2.pixels; 117 | final controller2Size = controller2.size; 118 | final controller2IsAttached = controller2.isAttached; 119 | // Close Sheet 2 by dragging it down 120 | await tester.fling( 121 | find.byType(DraggableScrollableSheet), 122 | const Offset(0, 500), 123 | 300, 124 | ); 125 | await tester.pumpAndSettle(); 126 | 127 | // Compare the initial values of the two controllers 128 | expect(controllerPixels, controller2Pixels); 129 | expect(controllerSize, controller2Size); 130 | expect(controllerIsAttached, controller2IsAttached); 131 | 132 | // Open Sheet 1 again and use the controller to scroll 133 | await tester.tap(find.text('Open Sheet 1')); 134 | await tester.pumpAndSettle(); 135 | const targetSize = 1.0; 136 | controller.jumpTo(targetSize); 137 | expect(targetSize, controller.size); 138 | }); 139 | }); 140 | } 141 | -------------------------------------------------------------------------------- /packages/flutter_hooks/test/use_expansible_controller_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/foundation.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter_hooks/src/framework.dart'; 4 | import 'package:flutter_hooks/src/hooks.dart'; 5 | 6 | import 'mock.dart'; 7 | 8 | void main() { 9 | testWidgets('debugFillProperties', (tester) async { 10 | await tester.pumpWidget( 11 | HookBuilder(builder: (context) { 12 | useExpansibleController(); 13 | return const SizedBox(); 14 | }), 15 | ); 16 | 17 | await tester.pump(); 18 | 19 | final element = tester.element(find.byType(HookBuilder)); 20 | 21 | expect( 22 | element 23 | .toDiagnosticsNode(style: DiagnosticsTreeStyle.offstage) 24 | .toStringDeep(), 25 | equalsIgnoringHashCodes( 26 | 'HookBuilder\n' 27 | " │ useExpansibleController: Instance of 'ExpansibleController'\n" 28 | ' └SizedBox(renderObject: RenderConstrainedBox#00000)\n', 29 | ), 30 | ); 31 | }); 32 | 33 | group('useExpansibleController', () { 34 | testWidgets('initial values matches with real constructor', (tester) async { 35 | late ExpansibleController controller; 36 | final controller2 = ExpansibleController(); 37 | 38 | await tester.pumpWidget(MaterialApp( 39 | home: Scaffold( 40 | body: HookBuilder(builder: (context) { 41 | controller = useExpansibleController(); 42 | return Column( 43 | children: [ 44 | ExpansionTile( 45 | controller: controller, 46 | title: const Text('Expansion Tile'), 47 | ), 48 | ExpansionTile( 49 | controller: controller2, 50 | title: const Text('Expansion Tile 2'), 51 | ), 52 | ], 53 | ); 54 | }), 55 | ), 56 | )); 57 | expect(controller, isA()); 58 | expect(controller.isExpanded, controller2.isExpanded); 59 | }); 60 | 61 | testWidgets('check expansion/collapse of tile', (tester) async { 62 | late ExpansibleController controller; 63 | await tester.pumpWidget(MaterialApp( 64 | home: Scaffold( 65 | body: HookBuilder(builder: (context) { 66 | controller = useExpansibleController(); 67 | return ExpansionTile( 68 | controller: controller, 69 | title: const Text('Expansion Tile'), 70 | ); 71 | }), 72 | ), 73 | )); 74 | 75 | expect(controller.isExpanded, false); 76 | controller.expand(); 77 | expect(controller.isExpanded, true); 78 | controller.collapse(); 79 | expect(controller.isExpanded, false); 80 | }); 81 | }); 82 | } 83 | -------------------------------------------------------------------------------- /packages/flutter_hooks/test/use_fixed_extent_scroll_controller_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/foundation.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter_hooks/src/framework.dart'; 4 | import 'package:flutter_hooks/src/hooks.dart'; 5 | 6 | import 'mock.dart'; 7 | 8 | void main() { 9 | testWidgets('debugFillProperties', (tester) async { 10 | await tester.pumpWidget( 11 | HookBuilder(builder: (context) { 12 | useFixedExtentScrollController(); 13 | return const SizedBox(); 14 | }), 15 | ); 16 | 17 | final element = tester.element(find.byType(HookBuilder)); 18 | 19 | expect( 20 | element 21 | .toDiagnosticsNode(style: DiagnosticsTreeStyle.offstage) 22 | .toStringDeep(), 23 | equalsIgnoringHashCodes( 24 | 'HookBuilder\n' 25 | ' │ useFixedExtentScrollController:\n' 26 | ' │ FixedExtentScrollController#00000(no clients)\n' 27 | ' └SizedBox(renderObject: RenderConstrainedBox#00000)\n', 28 | ), 29 | ); 30 | }); 31 | 32 | group('useFixedExtentScrollController', () { 33 | testWidgets('initial values matches with real constructor', (tester) async { 34 | late FixedExtentScrollController controller; 35 | late FixedExtentScrollController controller2; 36 | 37 | await tester.pumpWidget( 38 | HookBuilder(builder: (context) { 39 | controller2 = FixedExtentScrollController(); 40 | controller = useFixedExtentScrollController(); 41 | return Container(); 42 | }), 43 | ); 44 | 45 | expect(controller.debugLabel, controller2.debugLabel); 46 | expect(controller.initialItem, controller2.initialItem); 47 | expect(controller.onAttach, controller2.onAttach); 48 | expect(controller.onDetach, controller2.onDetach); 49 | }); 50 | testWidgets("returns a FixedExtentScrollController that doesn't change", 51 | (tester) async { 52 | late FixedExtentScrollController controller; 53 | late FixedExtentScrollController controller2; 54 | 55 | await tester.pumpWidget( 56 | HookBuilder(builder: (context) { 57 | controller2 = FixedExtentScrollController(); 58 | controller = useFixedExtentScrollController(); 59 | return Container(); 60 | }), 61 | ); 62 | expect(controller, isA()); 63 | 64 | await tester.pumpWidget( 65 | HookBuilder(builder: (context) { 66 | controller2 = useFixedExtentScrollController(); 67 | return Container(); 68 | }), 69 | ); 70 | 71 | expect(identical(controller, controller2), isTrue); 72 | }); 73 | 74 | testWidgets('passes hook parameters to the FixedExtentScrollController', 75 | (tester) async { 76 | late FixedExtentScrollController controller; 77 | 78 | void onAttach(ScrollPosition position) {} 79 | void onDetach(ScrollPosition position) {} 80 | 81 | await tester.pumpWidget( 82 | HookBuilder( 83 | builder: (context) { 84 | controller = useFixedExtentScrollController( 85 | initialItem: 42, 86 | onAttach: onAttach, 87 | onDetach: onDetach, 88 | ); 89 | 90 | return Container(); 91 | }, 92 | ), 93 | ); 94 | 95 | expect(controller.initialItem, 42); 96 | expect(controller.onAttach, onAttach); 97 | expect(controller.onDetach, onDetach); 98 | }); 99 | }); 100 | } 101 | 102 | class TickerProviderMock extends Mock implements TickerProvider {} 103 | -------------------------------------------------------------------------------- /packages/flutter_hooks/test/use_focus_node_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/foundation.dart'; 2 | import 'package:flutter/widgets.dart'; 3 | import 'package:flutter_hooks/flutter_hooks.dart'; 4 | 5 | import 'mock.dart'; 6 | 7 | void main() { 8 | testWidgets('creates a focus node and disposes it', (tester) async { 9 | late FocusNode focusNode; 10 | await tester.pumpWidget( 11 | HookBuilder(builder: (_) { 12 | focusNode = useFocusNode(); 13 | return Container(); 14 | }), 15 | ); 16 | 17 | expect(focusNode, isA()); 18 | // ignore: invalid_use_of_protected_member 19 | expect(focusNode.hasListeners, isFalse); 20 | 21 | final previousValue = focusNode; 22 | 23 | await tester.pumpWidget( 24 | HookBuilder(builder: (_) { 25 | focusNode = useFocusNode(); 26 | return Container(); 27 | }), 28 | ); 29 | 30 | expect(previousValue, focusNode); 31 | // check you can add listener (only possible if not disposed) 32 | focusNode.addListener(() {}); 33 | 34 | await tester.pumpWidget(Container()); 35 | 36 | expect( 37 | () => focusNode.addListener(() {}), 38 | throwsAssertionError, 39 | ); 40 | }); 41 | 42 | testWidgets('debugFillProperties', (tester) async { 43 | await tester.pumpWidget( 44 | HookBuilder(builder: (context) { 45 | useFocusNode(); 46 | return const SizedBox(); 47 | }), 48 | ); 49 | 50 | final element = tester.element(find.byType(HookBuilder)); 51 | 52 | expect( 53 | element 54 | .toDiagnosticsNode(style: DiagnosticsTreeStyle.offstage) 55 | .toStringDeep(), 56 | equalsIgnoringHashCodes( 57 | 'HookBuilder\n' 58 | ' │ useFocusNode: FocusNode#00000\n' 59 | ' └SizedBox(renderObject: RenderConstrainedBox#00000)\n', 60 | ), 61 | ); 62 | }); 63 | 64 | testWidgets('default values matches with FocusNode', (tester) async { 65 | final official = FocusNode(); 66 | 67 | late FocusNode focusNode; 68 | await tester.pumpWidget( 69 | HookBuilder(builder: (_) { 70 | focusNode = useFocusNode(); 71 | return Container(); 72 | }), 73 | ); 74 | 75 | expect(focusNode.debugLabel, official.debugLabel); 76 | expect(focusNode.skipTraversal, official.skipTraversal); 77 | expect(focusNode.canRequestFocus, official.canRequestFocus); 78 | expect(focusNode.descendantsAreFocusable, official.descendantsAreFocusable); 79 | }); 80 | 81 | testWidgets('has all the FocusNode parameters', (tester) async { 82 | KeyEventResult onKeyEvent(FocusNode node, KeyEvent event) => 83 | KeyEventResult.ignored; 84 | 85 | late FocusNode focusNode; 86 | await tester.pumpWidget( 87 | HookBuilder(builder: (_) { 88 | focusNode = useFocusNode( 89 | debugLabel: 'Foo', 90 | onKeyEvent: onKeyEvent, 91 | skipTraversal: true, 92 | canRequestFocus: false, 93 | descendantsAreFocusable: false, 94 | ); 95 | return Container(); 96 | }), 97 | ); 98 | 99 | expect(focusNode.debugLabel, 'Foo'); 100 | expect(focusNode.onKeyEvent, onKeyEvent); 101 | expect(focusNode.skipTraversal, true); 102 | expect(focusNode.canRequestFocus, false); 103 | expect(focusNode.descendantsAreFocusable, false); 104 | }); 105 | 106 | testWidgets('handles parameter change', (tester) async { 107 | KeyEventResult onKeyEvent(FocusNode node, KeyEvent event) => 108 | KeyEventResult.ignored; 109 | KeyEventResult onKeyEvent2(FocusNode node, KeyEvent event) => 110 | KeyEventResult.ignored; 111 | 112 | late FocusNode focusNode; 113 | await tester.pumpWidget( 114 | HookBuilder(builder: (_) { 115 | focusNode = useFocusNode( 116 | debugLabel: 'Foo', 117 | onKeyEvent: onKeyEvent, 118 | skipTraversal: true, 119 | canRequestFocus: false, 120 | descendantsAreFocusable: false, 121 | ); 122 | 123 | return Container(); 124 | }), 125 | ); 126 | 127 | await tester.pumpWidget( 128 | HookBuilder(builder: (_) { 129 | focusNode = useFocusNode( 130 | debugLabel: 'Bar', 131 | onKeyEvent: onKeyEvent2, 132 | ); 133 | 134 | return Container(); 135 | }), 136 | ); 137 | 138 | expect(focusNode.onKeyEvent, onKeyEvent2); 139 | expect(focusNode.debugLabel, 'Bar'); 140 | expect(focusNode.skipTraversal, false); 141 | expect(focusNode.canRequestFocus, true); 142 | expect(focusNode.descendantsAreFocusable, true); 143 | }); 144 | } 145 | -------------------------------------------------------------------------------- /packages/flutter_hooks/test/use_focus_scope_node_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/foundation.dart'; 2 | import 'package:flutter/widgets.dart'; 3 | import 'package:flutter_hooks/flutter_hooks.dart'; 4 | 5 | import 'mock.dart'; 6 | 7 | void main() { 8 | testWidgets('creates a focus scope node and disposes it', (tester) async { 9 | late FocusScopeNode focusScopeNode; 10 | await tester.pumpWidget( 11 | HookBuilder(builder: (_) { 12 | focusScopeNode = useFocusScopeNode(); 13 | return Container(); 14 | }), 15 | ); 16 | 17 | expect(focusScopeNode, isA()); 18 | // ignore: invalid_use_of_protected_member 19 | expect(focusScopeNode.hasListeners, isFalse); 20 | 21 | final previousValue = focusScopeNode; 22 | 23 | await tester.pumpWidget( 24 | HookBuilder(builder: (_) { 25 | focusScopeNode = useFocusScopeNode(); 26 | return Container(); 27 | }), 28 | ); 29 | 30 | expect(previousValue, focusScopeNode); 31 | // ignore: invalid_use_of_protected_member 32 | expect(focusScopeNode.hasListeners, isFalse); 33 | 34 | await tester.pumpWidget(Container()); 35 | 36 | expect( 37 | () => focusScopeNode.dispose(), 38 | throwsAssertionError, 39 | ); 40 | }); 41 | 42 | testWidgets('debugFillProperties', (tester) async { 43 | await tester.pumpWidget( 44 | HookBuilder(builder: (context) { 45 | useFocusScopeNode(); 46 | return const SizedBox(); 47 | }), 48 | ); 49 | 50 | final element = tester.element(find.byType(HookBuilder)); 51 | 52 | expect( 53 | element 54 | .toDiagnosticsNode(style: DiagnosticsTreeStyle.offstage) 55 | .toStringDeep(), 56 | equalsIgnoringHashCodes( 57 | 'HookBuilder\n' 58 | ' │ useFocusScopeNode: FocusScopeNode#00000\n' 59 | ' └SizedBox(renderObject: RenderConstrainedBox#00000)\n', 60 | ), 61 | ); 62 | }); 63 | 64 | testWidgets('default values matches with FocusScopeNode', (tester) async { 65 | final official = FocusScopeNode(); 66 | 67 | late FocusScopeNode focusScopeNode; 68 | await tester.pumpWidget( 69 | HookBuilder(builder: (_) { 70 | focusScopeNode = useFocusScopeNode(); 71 | return Container(); 72 | }), 73 | ); 74 | 75 | expect(focusScopeNode.debugLabel, official.debugLabel); 76 | expect(focusScopeNode.skipTraversal, official.skipTraversal); 77 | expect(focusScopeNode.canRequestFocus, official.canRequestFocus); 78 | }); 79 | 80 | testWidgets('has all the FocusScopeNode parameters', (tester) async { 81 | KeyEventResult onKeyEvent(FocusNode node, KeyEvent event) => 82 | KeyEventResult.ignored; 83 | 84 | late FocusScopeNode focusScopeNode; 85 | await tester.pumpWidget( 86 | HookBuilder(builder: (_) { 87 | focusScopeNode = useFocusScopeNode( 88 | debugLabel: 'Foo', 89 | onKeyEvent: onKeyEvent, 90 | skipTraversal: true, 91 | canRequestFocus: false, 92 | ); 93 | return Container(); 94 | }), 95 | ); 96 | 97 | expect(focusScopeNode.debugLabel, 'Foo'); 98 | expect(focusScopeNode.onKeyEvent, onKeyEvent); 99 | expect(focusScopeNode.skipTraversal, true); 100 | expect(focusScopeNode.canRequestFocus, false); 101 | }); 102 | 103 | testWidgets('handles parameter change', (tester) async { 104 | KeyEventResult onKeyEvent(FocusNode node, KeyEvent event) => 105 | KeyEventResult.ignored; 106 | KeyEventResult onKeyEvent2(FocusNode node, KeyEvent event) => 107 | KeyEventResult.ignored; 108 | 109 | late FocusScopeNode focusScopeNode; 110 | await tester.pumpWidget( 111 | HookBuilder(builder: (_) { 112 | focusScopeNode = useFocusScopeNode( 113 | debugLabel: 'Foo', 114 | onKeyEvent: onKeyEvent, 115 | skipTraversal: true, 116 | canRequestFocus: false, 117 | ); 118 | 119 | return Container(); 120 | }), 121 | ); 122 | 123 | await tester.pumpWidget( 124 | HookBuilder(builder: (_) { 125 | focusScopeNode = useFocusScopeNode( 126 | debugLabel: 'Bar', 127 | onKeyEvent: onKeyEvent2, 128 | ); 129 | 130 | return Container(); 131 | }), 132 | ); 133 | 134 | expect(focusScopeNode.onKeyEvent, onKeyEvent2); 135 | expect(focusScopeNode.debugLabel, 'Bar'); 136 | expect(focusScopeNode.skipTraversal, false); 137 | expect(focusScopeNode.canRequestFocus, true); 138 | }); 139 | } 140 | -------------------------------------------------------------------------------- /packages/flutter_hooks/test/use_listenable_selector_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/foundation.dart'; 2 | import 'package:flutter/widgets.dart'; 3 | import 'package:flutter_hooks/flutter_hooks.dart'; 4 | 5 | import 'mock.dart'; 6 | 7 | void main() { 8 | testWidgets('debugFillProperties', (tester) async { 9 | await tester.pumpWidget( 10 | HookBuilder( 11 | builder: (context) { 12 | final listenable = ValueNotifier(42); 13 | useListenableSelector(listenable, () => listenable.value.isOdd); 14 | return const SizedBox(); 15 | }, 16 | ), 17 | ); 18 | 19 | final element = tester.element(find.byType(HookBuilder)); 20 | 21 | expect( 22 | element 23 | .toDiagnosticsNode(style: DiagnosticsTreeStyle.offstage) 24 | .toStringDeep(), 25 | equalsIgnoringHashCodes( 26 | 'HookBuilder\n' 27 | ' │ useListenableSelector\n' 28 | ' └SizedBox(renderObject: RenderConstrainedBox#00000)\n' 29 | '', 30 | ), 31 | ); 32 | }); 33 | 34 | testWidgets('basic', (tester) async { 35 | final listenable = ValueNotifier(0); 36 | // ignore: prefer_function_declarations_over_variables 37 | final isOddSelector = () => listenable.value.isOdd; 38 | var isOdd = listenable.value.isOdd; 39 | 40 | await tester.pumpWidget( 41 | HookBuilder( 42 | builder: (context) { 43 | isOdd = useListenableSelector(listenable, isOddSelector); 44 | return Container(); 45 | }, 46 | ), 47 | ); 48 | 49 | final element = tester.firstElement(find.byType(HookBuilder)); 50 | 51 | // ignore: invalid_use_of_protected_member 52 | expect(listenable.hasListeners, true); 53 | expect(listenable.value, 0); 54 | expect(isOdd, false); 55 | expect(element.dirty, false); 56 | 57 | listenable.value++; 58 | 59 | expect(element.dirty, true); 60 | expect(listenable.value, 1); 61 | 62 | await tester.pump(); 63 | 64 | expect(isOdd, true); 65 | expect(element.dirty, false); 66 | 67 | listenable.value++; 68 | 69 | expect(element.dirty, true); 70 | 71 | await tester.pump(); 72 | 73 | expect(listenable.value, 2); 74 | expect(isOdd, false); 75 | 76 | listenable.value = listenable.value + 2; 77 | 78 | expect(element.dirty, false); 79 | 80 | await tester.pump(); 81 | 82 | expect(listenable.value, 4); 83 | expect(isOdd, false); 84 | 85 | listenable.dispose(); 86 | }); 87 | 88 | testWidgets('null as Listener', (tester) async { 89 | const notFoundValue = -1; 90 | final testListener = ValueNotifier(false); 91 | final listenable = ValueNotifier(777); 92 | var result = 0; 93 | 94 | await tester.pumpWidget( 95 | HookBuilder( 96 | builder: (context) { 97 | final shouldUseListener = useListenableSelector(testListener, () { 98 | return testListener.value; 99 | }); 100 | 101 | final actualListener = shouldUseListener ? listenable : null; 102 | 103 | result = useListenableSelector(actualListener, () { 104 | return actualListener?.value ?? notFoundValue; 105 | }); 106 | 107 | return Container(); 108 | }, 109 | ), 110 | ); 111 | 112 | expect(result, notFoundValue); 113 | testListener.value = true; 114 | 115 | await tester.pump(); 116 | 117 | expect(result, listenable.value); 118 | }); 119 | 120 | testWidgets('update selector', (tester) async { 121 | final listenable = ValueNotifier(0); 122 | var isOdd = false; 123 | // ignore: prefer_function_declarations_over_variables 124 | bool isOddSelector() => listenable.value.isOdd; 125 | var isEven = false; 126 | bool isEvenSelector() => listenable.value.isEven; 127 | 128 | await tester.pumpWidget( 129 | HookBuilder( 130 | builder: (context) { 131 | isOdd = useListenableSelector(listenable, isOddSelector); 132 | return Container(); 133 | }, 134 | ), 135 | ); 136 | 137 | final element = tester.firstElement(find.byType(HookBuilder)); 138 | 139 | // ignore: invalid_use_of_protected_member 140 | expect(listenable.hasListeners, true); 141 | expect(listenable.value, 0); 142 | expect(isOdd, false); 143 | expect(element.dirty, false); 144 | 145 | listenable.value++; 146 | 147 | expect(element.dirty, true); 148 | expect(listenable.value, 1); 149 | 150 | await tester.pump(); 151 | 152 | expect(isOdd, true); 153 | expect(element.dirty, false); 154 | 155 | await tester.pumpWidget( 156 | HookBuilder( 157 | builder: (context) { 158 | isEven = useListenableSelector(listenable, isEvenSelector); 159 | return Container(); 160 | }, 161 | ), 162 | ); 163 | 164 | expect(listenable.value, 1); 165 | expect(isEven, false); 166 | 167 | listenable.dispose(); 168 | }); 169 | 170 | testWidgets('update listenable', (tester) async { 171 | var listenable = ValueNotifier(0); 172 | bool isOddSelector() => listenable.value.isOdd; 173 | var isOdd = false; 174 | 175 | await tester.pumpWidget( 176 | HookBuilder( 177 | builder: (context) { 178 | isOdd = useListenableSelector(listenable, isOddSelector); 179 | return Container(); 180 | }, 181 | ), 182 | ); 183 | 184 | expect(isOdd, false); 185 | 186 | final previousListenable = listenable; 187 | listenable = ValueNotifier(1); 188 | 189 | await tester.pumpWidget( 190 | HookBuilder( 191 | builder: (context) { 192 | isOdd = useListenableSelector(listenable, isOddSelector); 193 | return Container(); 194 | }, 195 | ), 196 | ); 197 | 198 | // ignore: invalid_use_of_protected_member 199 | expect(previousListenable.hasListeners, false); 200 | expect(isOdd, true); 201 | 202 | listenable.dispose(); 203 | previousListenable.dispose(); 204 | }); 205 | } 206 | -------------------------------------------------------------------------------- /packages/flutter_hooks/test/use_listenable_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/foundation.dart'; 2 | import 'package:flutter/widgets.dart'; 3 | import 'package:flutter_hooks/flutter_hooks.dart'; 4 | 5 | import 'mock.dart'; 6 | 7 | void main() { 8 | testWidgets('debugFillProperties', (tester) async { 9 | await tester.pumpWidget( 10 | HookBuilder(builder: (context) { 11 | useListenable(const AlwaysStoppedAnimation(42)); 12 | return const SizedBox(); 13 | }), 14 | ); 15 | 16 | final element = tester.element(find.byType(HookBuilder)); 17 | 18 | expect( 19 | element 20 | .toDiagnosticsNode(style: DiagnosticsTreeStyle.offstage) 21 | .toStringDeep(), 22 | equalsIgnoringHashCodes( 23 | 'HookBuilder\n' 24 | ' │ useListenable: AlwaysStoppedAnimation#00000(▶ 42; paused)\n' 25 | ' └SizedBox(renderObject: RenderConstrainedBox#00000)\n', 26 | ), 27 | ); 28 | }); 29 | 30 | testWidgets('useListenable', (tester) async { 31 | var listenable = ValueNotifier(0); 32 | 33 | Future pump() { 34 | return tester.pumpWidget(HookBuilder( 35 | builder: (context) { 36 | useListenable(listenable); 37 | return Container(); 38 | }, 39 | )); 40 | } 41 | 42 | await pump(); 43 | 44 | final element = tester.firstElement(find.byType(HookBuilder)); 45 | 46 | // ignore: invalid_use_of_protected_member 47 | expect(listenable.hasListeners, true); 48 | expect(element.dirty, false); 49 | listenable.value++; 50 | expect(element.dirty, true); 51 | await tester.pump(); 52 | expect(element.dirty, false); 53 | 54 | final previousListenable = listenable; 55 | listenable = ValueNotifier(0); 56 | 57 | await pump(); 58 | 59 | // ignore: invalid_use_of_protected_member 60 | expect(previousListenable.hasListeners, false); 61 | // ignore: invalid_use_of_protected_member 62 | expect(listenable.hasListeners, true); 63 | expect(element.dirty, false); 64 | listenable.value++; 65 | expect(element.dirty, true); 66 | await tester.pump(); 67 | expect(element.dirty, false); 68 | 69 | await tester.pumpWidget(const SizedBox()); 70 | 71 | // ignore: invalid_use_of_protected_member 72 | expect(listenable.hasListeners, false); 73 | 74 | listenable.dispose(); 75 | previousListenable.dispose(); 76 | }); 77 | 78 | testWidgets('useListenable should handle null', (tester) async { 79 | ValueNotifier? listenable; 80 | 81 | Future pump() { 82 | return tester.pumpWidget(HookBuilder( 83 | builder: (context) { 84 | useListenable(listenable); 85 | return Container(); 86 | }, 87 | )); 88 | } 89 | 90 | await pump(); 91 | 92 | final element = tester.firstElement(find.byType(HookBuilder)); 93 | expect(element.dirty, false); 94 | 95 | final notifier = ValueNotifier(0); 96 | listenable = notifier; 97 | await pump(); 98 | 99 | // ignore: invalid_use_of_protected_member 100 | expect(listenable.hasListeners, true); 101 | 102 | listenable = null; 103 | await pump(); 104 | 105 | // ignore: invalid_use_of_protected_member 106 | expect(notifier.hasListeners, false); 107 | 108 | await tester.pumpWidget(const SizedBox()); 109 | 110 | notifier.dispose(); 111 | }); 112 | } 113 | -------------------------------------------------------------------------------- /packages/flutter_hooks/test/use_material_states_controller_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/foundation.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter_hooks/src/framework.dart'; 4 | import 'package:flutter_hooks/src/hooks.dart'; 5 | import 'package:flutter_test/flutter_test.dart'; 6 | 7 | import 'mock.dart'; 8 | 9 | void main() { 10 | testWidgets('debugFillProperties', (tester) async { 11 | await tester.pumpWidget( 12 | HookBuilder(builder: (context) { 13 | useWidgetStatesController(); 14 | return const SizedBox(); 15 | }), 16 | ); 17 | 18 | final element = tester.element(find.byType(HookBuilder)); 19 | 20 | expect( 21 | element 22 | .toDiagnosticsNode(style: DiagnosticsTreeStyle.offstage) 23 | .toStringDeep(), 24 | equalsIgnoringHashCodes( 25 | 'HookBuilder\n' 26 | ' │ useWidgetStatesController: WidgetStatesController#00000({})\n' 27 | ' └SizedBox(renderObject: RenderConstrainedBox#00000)\n', 28 | ), 29 | ); 30 | }); 31 | 32 | group('useWidgetStatesController', () { 33 | testWidgets('initial values matches with real constructor', (tester) async { 34 | late WidgetStatesController controller; 35 | late WidgetStatesController controller2; 36 | 37 | await tester.pumpWidget( 38 | HookBuilder(builder: (context) { 39 | controller2 = WidgetStatesController(); 40 | controller = useWidgetStatesController(); 41 | return Container(); 42 | }), 43 | ); 44 | 45 | expect(controller.value, controller2.value); 46 | }); 47 | testWidgets("returns a WidgetStatesController that doesn't change", 48 | (tester) async { 49 | late WidgetStatesController controller; 50 | late WidgetStatesController controller2; 51 | 52 | await tester.pumpWidget( 53 | HookBuilder(builder: (context) { 54 | controller = useWidgetStatesController(); 55 | return Container(); 56 | }), 57 | ); 58 | 59 | expect(controller, isA()); 60 | 61 | await tester.pumpWidget( 62 | HookBuilder(builder: (context) { 63 | controller2 = useWidgetStatesController(); 64 | return Container(); 65 | }), 66 | ); 67 | 68 | expect(identical(controller, controller2), isTrue); 69 | }); 70 | 71 | testWidgets('passes hook parameters to the WidgetStatesController', 72 | (tester) async { 73 | late WidgetStatesController controller; 74 | 75 | await tester.pumpWidget( 76 | HookBuilder( 77 | builder: (context) { 78 | controller = useWidgetStatesController( 79 | values: {WidgetState.selected}, 80 | ); 81 | 82 | return Container(); 83 | }, 84 | ), 85 | ); 86 | 87 | expect(controller.value, {WidgetState.selected}); 88 | }); 89 | 90 | testWidgets('disposes the WidgetStatesController on unmount', 91 | (tester) async { 92 | late WidgetStatesController controller; 93 | 94 | await tester.pumpWidget( 95 | HookBuilder( 96 | builder: (context) { 97 | controller = useWidgetStatesController(); 98 | return Container(); 99 | }, 100 | ), 101 | ); 102 | 103 | // pump another widget so that the old one gets disposed 104 | await tester.pumpWidget(Container()); 105 | 106 | expect( 107 | () => controller.addListener(() {}), 108 | throwsA(isFlutterError.having( 109 | (e) => e.message, 'message', contains('disposed'))), 110 | ); 111 | }); 112 | }); 113 | } 114 | -------------------------------------------------------------------------------- /packages/flutter_hooks/test/use_on_listenable_change_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/foundation.dart'; 2 | import 'package:flutter/widgets.dart'; 3 | import 'package:flutter_hooks/flutter_hooks.dart'; 4 | 5 | import 'mock.dart'; 6 | 7 | void main() { 8 | testWidgets('debugFillProperties', (tester) async { 9 | final listenable = ValueNotifier(42); 10 | 11 | await tester.pumpWidget( 12 | HookBuilder(builder: (context) { 13 | useOnListenableChange(listenable, () {}); 14 | return const SizedBox(); 15 | }), 16 | ); 17 | 18 | await tester.pump(); 19 | 20 | final element = tester.element(find.byType(HookBuilder)); 21 | 22 | expect( 23 | element 24 | .toDiagnosticsNode(style: DiagnosticsTreeStyle.offstage) 25 | .toStringDeep(), 26 | equalsIgnoringHashCodes( 27 | 'HookBuilder\n' 28 | ' │ useOnListenableChange: ValueNotifier#00000(42)\n' 29 | ' └SizedBox(renderObject: RenderConstrainedBox#00000)\n', 30 | ), 31 | ); 32 | }); 33 | 34 | testWidgets('calls listener when Listenable updates', (tester) async { 35 | final listenable = ValueNotifier(42); 36 | 37 | int? value; 38 | 39 | await tester.pumpWidget( 40 | HookBuilder(builder: (context) { 41 | useOnListenableChange( 42 | listenable, 43 | () => value = listenable.value, 44 | ); 45 | return const SizedBox(); 46 | }), 47 | ); 48 | 49 | expect(value, isNull); 50 | listenable.value++; 51 | expect(value, 43); 52 | }); 53 | 54 | testWidgets( 55 | 'listens new Listenable when Listenable is changed', 56 | (tester) async { 57 | final listenable1 = ValueNotifier(42); 58 | final listenable2 = ValueNotifier(42); 59 | 60 | await tester.pumpWidget( 61 | HookBuilder( 62 | builder: (context) { 63 | useOnListenableChange(listenable1, () {}); 64 | return const SizedBox(); 65 | }, 66 | ), 67 | ); 68 | 69 | await tester.pumpWidget( 70 | HookBuilder( 71 | builder: (context) { 72 | useOnListenableChange(listenable2, () {}); 73 | return const SizedBox(); 74 | }, 75 | ), 76 | ); 77 | 78 | // ignore: invalid_use_of_protected_member 79 | expect(listenable1.hasListeners, isFalse); 80 | // ignore: invalid_use_of_protected_member 81 | expect(listenable2.hasListeners, isTrue); 82 | }, 83 | ); 84 | 85 | testWidgets( 86 | 'listens new listener when listener is changed', 87 | (tester) async { 88 | final listenable = ValueNotifier(42); 89 | late final int value; 90 | 91 | void listener1() { 92 | throw StateError('listener1 should not have been called'); 93 | } 94 | 95 | void listener2() { 96 | value = listenable.value; 97 | } 98 | 99 | await tester.pumpWidget( 100 | HookBuilder( 101 | builder: (context) { 102 | useOnListenableChange(listenable, listener1); 103 | return const SizedBox(); 104 | }, 105 | ), 106 | ); 107 | 108 | await tester.pumpWidget( 109 | HookBuilder( 110 | builder: (context) { 111 | useOnListenableChange(listenable, listener2); 112 | return const SizedBox(); 113 | }, 114 | ), 115 | ); 116 | 117 | listenable.value++; 118 | // By now, we should have subscribed to listener2, which sets the value 119 | expect(value, 43); 120 | }, 121 | ); 122 | 123 | testWidgets('unsubscribes when listenable becomes null', (tester) async { 124 | final listenable = ValueNotifier(42); 125 | 126 | await tester.pumpWidget( 127 | HookBuilder(builder: (context) { 128 | useOnListenableChange(listenable, () {}); 129 | return const SizedBox(); 130 | }), 131 | ); 132 | 133 | // ignore: invalid_use_of_protected_member 134 | expect(listenable.hasListeners, isTrue); 135 | 136 | await tester.pumpWidget( 137 | HookBuilder(builder: (context) { 138 | useOnListenableChange(null, () {}); 139 | return const SizedBox(); 140 | }), 141 | ); 142 | 143 | // ignore: invalid_use_of_protected_member 144 | expect(listenable.hasListeners, isFalse); 145 | }); 146 | 147 | testWidgets('unsubscribes when disposed', (tester) async { 148 | final listenable = ValueNotifier(42); 149 | 150 | await tester.pumpWidget( 151 | HookBuilder(builder: (context) { 152 | useOnListenableChange(listenable, () {}); 153 | return const SizedBox(); 154 | }), 155 | ); 156 | 157 | // ignore: invalid_use_of_protected_member 158 | expect(listenable.hasListeners, isTrue); 159 | 160 | await tester.pumpWidget(Container()); 161 | 162 | // ignore: invalid_use_of_protected_member 163 | expect(listenable.hasListeners, isFalse); 164 | }); 165 | } 166 | -------------------------------------------------------------------------------- /packages/flutter_hooks/test/use_overlay_portal_controller_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/foundation.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter_hooks/src/framework.dart'; 4 | import 'package:flutter_hooks/src/hooks.dart'; 5 | 6 | import 'mock.dart'; 7 | 8 | void main() { 9 | testWidgets('debugFillProperties', (tester) async { 10 | await tester.pumpWidget( 11 | HookBuilder(builder: (context) { 12 | useOverlayPortalController(); 13 | return const SizedBox(); 14 | }), 15 | ); 16 | 17 | await tester.pump(); 18 | 19 | final element = tester.element(find.byType(HookBuilder)); 20 | 21 | expect( 22 | element 23 | .toDiagnosticsNode(style: DiagnosticsTreeStyle.offstage) 24 | .toStringDeep(), 25 | equalsIgnoringHashCodes( 26 | 'HookBuilder\n' 27 | ' │ useOverlayPortalController: OverlayPortalController DETACHED\n' 28 | ' └SizedBox(renderObject: RenderConstrainedBox#00000)\n', 29 | ), 30 | ); 31 | }); 32 | 33 | group('useOverlayPortalController', () { 34 | testWidgets('initial values matches with real constructor', (tester) async { 35 | late OverlayPortalController controller; 36 | final controller2 = OverlayPortalController(); 37 | 38 | await tester.pumpWidget(MaterialApp( 39 | home: Scaffold( 40 | body: HookBuilder(builder: (context) { 41 | controller = useOverlayPortalController(); 42 | return Column( 43 | children: [ 44 | OverlayPortal( 45 | controller: controller, 46 | overlayChildBuilder: (context) => 47 | const Text('Overlay Portal'), 48 | ), 49 | OverlayPortal( 50 | controller: controller2, 51 | overlayChildBuilder: (context) => 52 | const Text('Overlay Portal 2'), 53 | ), 54 | ], 55 | ); 56 | }), 57 | ), 58 | )); 59 | expect(controller, isA()); 60 | expect(controller.isShowing, controller2.isShowing); 61 | }); 62 | 63 | testWidgets('check show/hide of overlay portal', (tester) async { 64 | late OverlayPortalController controller; 65 | await tester.pumpWidget(MaterialApp( 66 | home: Scaffold( 67 | body: HookBuilder(builder: (context) { 68 | controller = useOverlayPortalController(); 69 | return OverlayPortal( 70 | controller: controller, 71 | overlayChildBuilder: (context) => const Text('Overlay Content'), 72 | ); 73 | }), 74 | ), 75 | )); 76 | 77 | expect(controller.isShowing, false); 78 | expect(find.text('Overlay Content'), findsNothing); 79 | 80 | controller.show(); 81 | await tester.pump(); 82 | expect(controller.isShowing, true); 83 | expect(find.text('Overlay Content'), findsOneWidget); 84 | 85 | controller.hide(); 86 | await tester.pump(); 87 | expect(controller.isShowing, false); 88 | expect(find.text('Overlay Content'), findsNothing); 89 | }); 90 | }); 91 | } 92 | -------------------------------------------------------------------------------- /packages/flutter_hooks/test/use_page_controller_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/foundation.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter_hooks/src/framework.dart'; 4 | import 'package:flutter_hooks/src/hooks.dart'; 5 | import 'package:flutter_test/flutter_test.dart'; 6 | 7 | import 'mock.dart'; 8 | 9 | void main() { 10 | testWidgets('debugFillProperties', (tester) async { 11 | await tester.pumpWidget( 12 | HookBuilder(builder: (context) { 13 | usePageController(); 14 | return const SizedBox(); 15 | }), 16 | ); 17 | 18 | final element = tester.element(find.byType(HookBuilder)); 19 | 20 | expect( 21 | element 22 | .toDiagnosticsNode(style: DiagnosticsTreeStyle.offstage) 23 | .toStringDeep(), 24 | equalsIgnoringHashCodes( 25 | 'HookBuilder\n' 26 | ' │ usePageController: PageController#00000(no clients)\n' 27 | ' └SizedBox(renderObject: RenderConstrainedBox#00000)\n', 28 | ), 29 | ); 30 | }); 31 | 32 | group('usePageController', () { 33 | testWidgets('initial values matches with real constructor', (tester) async { 34 | late PageController controller; 35 | late PageController controller2; 36 | 37 | await tester.pumpWidget( 38 | HookBuilder(builder: (context) { 39 | controller2 = PageController(); 40 | controller = usePageController(); 41 | return Container(); 42 | }), 43 | ); 44 | 45 | expect(controller.initialPage, controller2.initialPage); 46 | expect(controller.keepPage, controller2.keepPage); 47 | expect(controller.viewportFraction, controller2.viewportFraction); 48 | expect(controller.onAttach, controller2.onAttach); 49 | expect(controller.onDetach, controller2.onDetach); 50 | }); 51 | testWidgets("returns a PageController that doesn't change", (tester) async { 52 | late PageController controller; 53 | late PageController controller2; 54 | 55 | await tester.pumpWidget( 56 | HookBuilder(builder: (context) { 57 | controller = usePageController(); 58 | return Container(); 59 | }), 60 | ); 61 | 62 | expect(controller, isA()); 63 | 64 | await tester.pumpWidget( 65 | HookBuilder(builder: (context) { 66 | controller2 = usePageController(); 67 | return Container(); 68 | }), 69 | ); 70 | 71 | expect(identical(controller, controller2), isTrue); 72 | }); 73 | 74 | testWidgets('passes hook parameters to the PageController', (tester) async { 75 | late PageController controller; 76 | 77 | void onAttach(ScrollPosition position) {} 78 | void onDetach(ScrollPosition position) {} 79 | 80 | await tester.pumpWidget( 81 | HookBuilder( 82 | builder: (context) { 83 | controller = usePageController( 84 | initialPage: 42, 85 | keepPage: false, 86 | viewportFraction: 3.4, 87 | onAttach: onAttach, 88 | onDetach: onDetach, 89 | ); 90 | 91 | return Container(); 92 | }, 93 | ), 94 | ); 95 | 96 | expect(controller.initialPage, 42); 97 | expect(controller.keepPage, false); 98 | expect(controller.viewportFraction, 3.4); 99 | expect(controller.onAttach, onAttach); 100 | expect(controller.onDetach, onDetach); 101 | }); 102 | 103 | testWidgets('disposes the PageController on unmount', (tester) async { 104 | late PageController controller; 105 | 106 | await tester.pumpWidget( 107 | HookBuilder( 108 | builder: (context) { 109 | controller = usePageController(); 110 | return Container(); 111 | }, 112 | ), 113 | ); 114 | 115 | // pump another widget so that the old one gets disposed 116 | await tester.pumpWidget(Container()); 117 | 118 | expect( 119 | () => controller.addListener(() {}), 120 | throwsA(isFlutterError.having( 121 | (e) => e.message, 'message', contains('disposed'))), 122 | ); 123 | }); 124 | }); 125 | } 126 | -------------------------------------------------------------------------------- /packages/flutter_hooks/test/use_platform_brightness_test.dart: -------------------------------------------------------------------------------- 1 | import 'dart:ui'; 2 | 3 | import 'package:flutter/widgets.dart'; 4 | import 'package:flutter_hooks/flutter_hooks.dart'; 5 | 6 | import 'mock.dart'; 7 | 8 | void main() { 9 | group('usePlatformBrightness', () { 10 | testWidgets('returns initial value and rebuild widgets on change', 11 | (tester) async { 12 | final binding = tester.binding; 13 | binding.platformDispatcher.platformBrightnessTestValue = Brightness.light; 14 | 15 | await tester.pumpWidget( 16 | HookBuilder( 17 | builder: (context) { 18 | final brightness = usePlatformBrightness(); 19 | return Text('$brightness', textDirection: TextDirection.ltr); 20 | }, 21 | ), 22 | ); 23 | 24 | expect(find.text('Brightness.light'), findsOneWidget); 25 | 26 | binding.platformDispatcher.platformBrightnessTestValue = Brightness.dark; 27 | await tester.pump(); 28 | 29 | expect(find.text('Brightness.dark'), findsOneWidget); 30 | }); 31 | }); 32 | 33 | group('useOnPlatformBrightnessChange', () { 34 | testWidgets( 35 | 'sends previous and new value on change, without rebuilding widgets', 36 | (tester) async { 37 | final binding = tester.binding; 38 | binding.platformDispatcher.platformBrightnessTestValue = Brightness.light; 39 | var buildCount = 0; 40 | final listener = PlatformBrightnessListener(); 41 | 42 | await tester.pumpWidget( 43 | HookBuilder( 44 | builder: (context) { 45 | buildCount++; 46 | useOnPlatformBrightnessChange(listener); 47 | return Container(); 48 | }, 49 | ), 50 | ); 51 | 52 | expect(buildCount, 1); 53 | verifyZeroInteractions(listener); 54 | 55 | binding.platformDispatcher.platformBrightnessTestValue = Brightness.dark; 56 | await tester.pump(); 57 | 58 | expect(buildCount, 1); 59 | verify(listener(Brightness.light, Brightness.dark)); 60 | verifyNoMoreInteractions(listener); 61 | 62 | binding.platformDispatcher.platformBrightnessTestValue = Brightness.light; 63 | await tester.pump(); 64 | 65 | expect(buildCount, 1); 66 | verify(listener(Brightness.dark, Brightness.light)); 67 | verifyNoMoreInteractions(listener); 68 | }); 69 | }); 70 | } 71 | 72 | class PlatformBrightnessListener extends Mock { 73 | void call(Brightness previous, Brightness current); 74 | } 75 | -------------------------------------------------------------------------------- /packages/flutter_hooks/test/use_previous_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/foundation.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter_hooks/flutter_hooks.dart'; 4 | import 'package:flutter_test/flutter_test.dart'; 5 | 6 | Widget build(int value) => HookBuilder( 7 | builder: (context) => 8 | Text(usePrevious(value).toString(), textDirection: TextDirection.ltr), 9 | ); 10 | void main() { 11 | group('usePrevious', () { 12 | testWidgets('default value is null', (tester) async { 13 | await tester.pumpWidget(build(0)); 14 | 15 | expect(find.text('null'), findsOneWidget); 16 | }); 17 | testWidgets('subsequent build returns previous value', (tester) async { 18 | await tester.pumpWidget(build(0)); 19 | await tester.pumpWidget(build(1)); 20 | 21 | expect(find.text('0'), findsOneWidget); 22 | 23 | await tester.pumpWidget(build(1)); 24 | 25 | expect(find.text('1'), findsOneWidget); 26 | 27 | await tester.pumpWidget(build(2)); 28 | expect(find.text('1'), findsOneWidget); 29 | 30 | await tester.pumpWidget(build(3)); 31 | expect(find.text('2'), findsOneWidget); 32 | }); 33 | }); 34 | 35 | testWidgets('debugFillProperties', (tester) async { 36 | await tester.pumpWidget( 37 | HookBuilder(builder: (context) { 38 | usePrevious(42); 39 | return const SizedBox(); 40 | }), 41 | ); 42 | 43 | await tester.pumpWidget( 44 | HookBuilder(builder: (context) { 45 | usePrevious(21); 46 | return const SizedBox(); 47 | }), 48 | ); 49 | 50 | final element = tester.element(find.byType(HookBuilder)); 51 | 52 | expect( 53 | element 54 | .toDiagnosticsNode(style: DiagnosticsTreeStyle.offstage) 55 | .toStringDeep(), 56 | equalsIgnoringHashCodes( 57 | 'HookBuilder\n' 58 | ' │ usePrevious: 42\n' 59 | ' └SizedBox(renderObject: RenderConstrainedBox#00000)\n', 60 | ), 61 | ); 62 | }); 63 | } 64 | -------------------------------------------------------------------------------- /packages/flutter_hooks/test/use_reassemble_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/foundation.dart'; 2 | import 'package:flutter/widgets.dart'; 3 | import 'package:flutter_hooks/flutter_hooks.dart'; 4 | 5 | import 'mock.dart'; 6 | 7 | void main() { 8 | testWidgets("hot-reload calls useReassemble's callback", (tester) async { 9 | final reassemble = MockReassemble(); 10 | 11 | await tester.pumpWidget(HookBuilder(builder: (context) { 12 | useReassemble(reassemble); 13 | return Container(); 14 | })); 15 | 16 | verifyNoMoreInteractions(reassemble); 17 | 18 | hotReload(tester); 19 | await tester.pump(); 20 | 21 | verify(reassemble()).called(1); 22 | verifyNoMoreInteractions(reassemble); 23 | }); 24 | 25 | testWidgets('debugFillProperties', (tester) async { 26 | await tester.pumpWidget( 27 | HookBuilder(builder: (context) { 28 | useReassemble(() {}); 29 | return const SizedBox(); 30 | }), 31 | ); 32 | 33 | final element = tester.element(find.byType(HookBuilder)); 34 | 35 | expect( 36 | element 37 | .toDiagnosticsNode(style: DiagnosticsTreeStyle.offstage) 38 | .toStringDeep(), 39 | equalsIgnoringHashCodes( 40 | 'HookBuilder\n' 41 | ' │ useReassemble\n' 42 | ' └SizedBox(renderObject: RenderConstrainedBox#00000)\n', 43 | ), 44 | ); 45 | }); 46 | } 47 | -------------------------------------------------------------------------------- /packages/flutter_hooks/test/use_reducer_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/foundation.dart'; 2 | import 'package:flutter/widgets.dart'; 3 | import 'package:flutter_hooks/flutter_hooks.dart'; 4 | 5 | import 'mock.dart'; 6 | 7 | void main() { 8 | testWidgets('debugFillProperties', (tester) async { 9 | await tester.pumpWidget( 10 | HookBuilder(builder: (context) { 11 | useReducer( 12 | (state, action) => 42, 13 | initialAction: null, 14 | initialState: null, 15 | ); 16 | return const SizedBox(); 17 | }), 18 | ); 19 | 20 | final element = tester.element(find.byType(HookBuilder)); 21 | 22 | expect( 23 | element 24 | .toDiagnosticsNode(style: DiagnosticsTreeStyle.offstage) 25 | .toStringDeep(), 26 | equalsIgnoringHashCodes( 27 | 'HookBuilder\n' 28 | ' │ useReducer: 42\n' 29 | ' └SizedBox(renderObject: RenderConstrainedBox#00000)\n', 30 | ), 31 | ); 32 | }); 33 | 34 | group('useReducer', () { 35 | testWidgets('supports null initial state', (tester) async { 36 | Store? store; 37 | 38 | await tester.pumpWidget( 39 | HookBuilder( 40 | builder: (context) { 41 | store = useReducer( 42 | (state, action) => state, 43 | initialAction: null, 44 | initialState: null, 45 | ); 46 | 47 | return Container(); 48 | }, 49 | ), 50 | ); 51 | 52 | expect(store!.state, isNull); 53 | }); 54 | 55 | testWidgets('supports null state after dispatch', (tester) async { 56 | Store? store; 57 | 58 | await tester.pumpWidget( 59 | HookBuilder( 60 | builder: (context) { 61 | store = useReducer( 62 | (state, action) => action, 63 | initialAction: 0, 64 | initialState: null, 65 | ); 66 | 67 | return Container(); 68 | }, 69 | ), 70 | ); 71 | 72 | expect(store?.state, 0); 73 | 74 | store!.dispatch(null); 75 | 76 | expect(store!.state, null); 77 | }); 78 | 79 | testWidgets('initialize the state even "state" is never read', 80 | (tester) async { 81 | final reducer = MockReducer(); 82 | 83 | await tester.pumpWidget( 84 | HookBuilder( 85 | builder: (context) { 86 | useReducer( 87 | reducer, 88 | initialAction: '', 89 | initialState: 0, 90 | ); 91 | return Container(); 92 | }, 93 | ), 94 | ); 95 | 96 | verify(reducer(null, null)).called(1); 97 | verifyNoMoreInteractions(reducer); 98 | }); 99 | 100 | testWidgets('basic', (tester) async { 101 | final reducer = MockReducer(); 102 | 103 | Store? store; 104 | 105 | Future pump() { 106 | return tester.pumpWidget( 107 | HookBuilder( 108 | builder: (context) { 109 | store = useReducer( 110 | reducer, 111 | initialAction: null, 112 | initialState: null, 113 | ); 114 | return Container(); 115 | }, 116 | ), 117 | ); 118 | } 119 | 120 | when(reducer(null, null)).thenReturn(0); 121 | 122 | await pump(); 123 | final element = tester.firstElement(find.byType(HookBuilder)); 124 | 125 | verify(reducer(null, null)).called(1); 126 | verifyNoMoreInteractions(reducer); 127 | 128 | expect(store!.state, 0); 129 | 130 | await pump(); 131 | 132 | verifyNoMoreInteractions(reducer); 133 | expect(store!.state, 0); 134 | 135 | when(reducer(0, 'foo')).thenReturn(1); 136 | 137 | store!.dispatch('foo'); 138 | 139 | verify(reducer(0, 'foo')).called(1); 140 | verifyNoMoreInteractions(reducer); 141 | expect(element.dirty, true); 142 | 143 | await pump(); 144 | 145 | when(reducer(1, 'bar')).thenReturn(1); 146 | 147 | store!.dispatch('bar'); 148 | 149 | verify(reducer(1, 'bar')).called(1); 150 | verifyNoMoreInteractions(reducer); 151 | expect(element.dirty, false); 152 | }); 153 | 154 | testWidgets('dispatch during build works', (tester) async { 155 | Store? store; 156 | 157 | await tester.pumpWidget( 158 | HookBuilder( 159 | builder: (context) { 160 | store = useReducer( 161 | (state, action) => action, 162 | initialAction: 0, 163 | initialState: null, 164 | )..dispatch(42); 165 | return Container(); 166 | }, 167 | ), 168 | ); 169 | 170 | expect(store!.state, 42); 171 | }); 172 | 173 | testWidgets('first reducer call receive initialAction and initialState', 174 | (tester) async { 175 | final reducer = MockReducer(); 176 | when(reducer(0, 'Foo')).thenReturn(42); 177 | 178 | await tester.pumpWidget( 179 | HookBuilder( 180 | builder: (context) { 181 | final result = useReducer( 182 | reducer, 183 | initialAction: 'Foo', 184 | initialState: 0, 185 | ).state; 186 | return Text('$result', textDirection: TextDirection.ltr); 187 | }, 188 | ), 189 | ); 190 | 191 | expect(find.text('42'), findsOneWidget); 192 | }); 193 | }); 194 | } 195 | 196 | class MockReducer extends Mock { 197 | int? call(int? state, String? action) { 198 | return super.noSuchMethod( 199 | Invocation.getter(#call), 200 | returnValue: 0, 201 | ) as int?; 202 | } 203 | } 204 | -------------------------------------------------------------------------------- /packages/flutter_hooks/test/use_scroll_controller_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/foundation.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter_hooks/src/framework.dart'; 4 | import 'package:flutter_hooks/src/hooks.dart'; 5 | 6 | import 'mock.dart'; 7 | 8 | void main() { 9 | testWidgets('debugFillProperties', (tester) async { 10 | await tester.pumpWidget( 11 | HookBuilder(builder: (context) { 12 | useScrollController(); 13 | return const SizedBox(); 14 | }), 15 | ); 16 | 17 | final element = tester.element(find.byType(HookBuilder)); 18 | 19 | expect( 20 | element 21 | .toDiagnosticsNode(style: DiagnosticsTreeStyle.offstage) 22 | .toStringDeep(), 23 | equalsIgnoringHashCodes( 24 | 'HookBuilder\n' 25 | ' │ useScrollController: ScrollController#00000(no clients)\n' 26 | ' └SizedBox(renderObject: RenderConstrainedBox#00000)\n', 27 | ), 28 | ); 29 | }); 30 | 31 | group('useScrollController', () { 32 | testWidgets('initial values matches with real constructor', (tester) async { 33 | late ScrollController controller; 34 | late ScrollController controller2; 35 | 36 | await tester.pumpWidget( 37 | HookBuilder(builder: (context) { 38 | controller2 = ScrollController(); 39 | controller = useScrollController(); 40 | return Container(); 41 | }), 42 | ); 43 | 44 | expect(controller.debugLabel, controller2.debugLabel); 45 | expect(controller.initialScrollOffset, controller2.initialScrollOffset); 46 | expect(controller.keepScrollOffset, controller2.keepScrollOffset); 47 | expect(controller.onAttach, controller2.onAttach); 48 | expect(controller.onDetach, controller2.onDetach); 49 | }); 50 | testWidgets("returns a ScrollController that doesn't change", 51 | (tester) async { 52 | late ScrollController controller; 53 | late ScrollController controller2; 54 | 55 | await tester.pumpWidget( 56 | HookBuilder(builder: (context) { 57 | controller = useScrollController(); 58 | return Container(); 59 | }), 60 | ); 61 | 62 | expect(controller, isA()); 63 | 64 | await tester.pumpWidget( 65 | HookBuilder(builder: (context) { 66 | controller2 = useScrollController(); 67 | return Container(); 68 | }), 69 | ); 70 | 71 | expect(identical(controller, controller2), isTrue); 72 | }); 73 | 74 | testWidgets('passes hook parameters to the ScrollController', 75 | (tester) async { 76 | late ScrollController controller; 77 | 78 | void onAttach(ScrollPosition position) {} 79 | void onDetach(ScrollPosition position) {} 80 | 81 | await tester.pumpWidget( 82 | HookBuilder( 83 | builder: (context) { 84 | controller = useScrollController( 85 | initialScrollOffset: 42, 86 | debugLabel: 'Hello', 87 | keepScrollOffset: false, 88 | onAttach: onAttach, 89 | onDetach: onDetach, 90 | ); 91 | 92 | return Container(); 93 | }, 94 | ), 95 | ); 96 | 97 | expect(controller.initialScrollOffset, 42); 98 | expect(controller.debugLabel, 'Hello'); 99 | expect(controller.keepScrollOffset, false); 100 | expect(controller.onAttach, onAttach); 101 | expect(controller.onDetach, onDetach); 102 | }); 103 | }); 104 | } 105 | 106 | class TickerProviderMock extends Mock implements TickerProvider {} 107 | -------------------------------------------------------------------------------- /packages/flutter_hooks/test/use_search_controller_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/foundation.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter_hooks/src/framework.dart'; 4 | import 'package:flutter_hooks/src/hooks.dart'; 5 | 6 | import 'mock.dart'; 7 | 8 | void main() { 9 | testWidgets('debugFillProperties', (tester) async { 10 | await tester.pumpWidget( 11 | HookBuilder(builder: (context) { 12 | useSearchController(); 13 | 14 | return const SizedBox(); 15 | }), 16 | ); 17 | 18 | await tester.pump(); 19 | 20 | final element = tester.element(find.byType(HookBuilder)); 21 | 22 | expect( 23 | element 24 | .toDiagnosticsNode(style: DiagnosticsTreeStyle.offstage) 25 | .toStringDeep(), 26 | equalsIgnoringHashCodes( 27 | 'HookBuilder\n' 28 | ' │ useSearchController:\n' 29 | ' │ SearchController#00000(TextEditingValue(text: ┤├, selection:\n' 30 | ' │ TextSelection.invalid, composing: TextRange(start: -1, end:\n' 31 | ' │ -1)))\n' 32 | ' └SizedBox(renderObject: RenderConstrainedBox#00000)\n', 33 | ), 34 | ); 35 | }); 36 | 37 | group('useSearchController', () { 38 | testWidgets('initial values matches with real constructor', (tester) async { 39 | late SearchController controller; 40 | final controller2 = SearchController(); 41 | 42 | await tester.pumpWidget( 43 | HookBuilder(builder: (context) { 44 | controller = useSearchController(); 45 | 46 | return Container(); 47 | }), 48 | ); 49 | 50 | expect(controller, isA()); 51 | 52 | expect(controller.selection, controller2.selection); 53 | expect(controller.text, controller2.text); 54 | expect(controller.value, controller2.value); 55 | }); 56 | 57 | testWidgets('check opening/closing view', (tester) async { 58 | late SearchController controller; 59 | 60 | await tester.pumpWidget(MaterialApp( 61 | home: HookBuilder(builder: (context) { 62 | controller = useSearchController(); 63 | 64 | return SearchAnchor.bar( 65 | searchController: controller, 66 | suggestionsBuilder: (context, controller) => [], 67 | ); 68 | }), 69 | )); 70 | 71 | controller.openView(); 72 | 73 | expect(controller.isOpen, true); 74 | 75 | await tester.pumpWidget(MaterialApp( 76 | home: HookBuilder(builder: (context) { 77 | controller = useSearchController(); 78 | 79 | return SearchAnchor.bar( 80 | searchController: controller, 81 | suggestionsBuilder: (context, controller) => [], 82 | ); 83 | }), 84 | )); 85 | 86 | controller.closeView('selected'); 87 | 88 | expect(controller.isOpen, false); 89 | expect(controller.text, 'selected'); 90 | }); 91 | }); 92 | } 93 | -------------------------------------------------------------------------------- /packages/flutter_hooks/test/use_state_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter/widgets.dart'; 3 | import 'package:flutter_hooks/flutter_hooks.dart'; 4 | 5 | import 'mock.dart'; 6 | 7 | void main() { 8 | testWidgets('useState basic', (tester) async { 9 | late ValueNotifier state; 10 | late HookElement element; 11 | 12 | await tester.pumpWidget(HookBuilder( 13 | builder: (context) { 14 | element = context as HookElement; 15 | state = useState(42); 16 | return Container(); 17 | }, 18 | )); 19 | 20 | expect(state.value, 42); 21 | expect(element.dirty, false); 22 | 23 | await tester.pump(); 24 | 25 | expect(state.value, 42); 26 | expect(element.dirty, false); 27 | 28 | state.value++; 29 | expect(element.dirty, true); 30 | await tester.pump(); 31 | 32 | expect(state.value, 43); 33 | expect(element.dirty, false); 34 | 35 | // dispose 36 | await tester.pumpWidget(const SizedBox()); 37 | 38 | expect(() => state.addListener(() {}), throwsFlutterError); 39 | }); 40 | 41 | testWidgets('no initial data', (tester) async { 42 | late ValueNotifier state; 43 | late HookElement element; 44 | 45 | await tester.pumpWidget(HookBuilder( 46 | builder: (context) { 47 | element = context as HookElement; 48 | state = useState(null); 49 | return Container(); 50 | }, 51 | )); 52 | 53 | expect(state.value, null); 54 | expect(element.dirty, false); 55 | 56 | await tester.pump(); 57 | 58 | expect(state.value, null); 59 | expect(element.dirty, false); 60 | 61 | state.value = 43; 62 | expect(element.dirty, true); 63 | await tester.pump(); 64 | 65 | expect(state.value, 43); 66 | expect(element.dirty, false); 67 | 68 | // dispose 69 | await tester.pumpWidget(const SizedBox()); 70 | 71 | expect(() => state.addListener(() {}), throwsFlutterError); 72 | }); 73 | 74 | testWidgets('debugFillProperties should print state hook ', (tester) async { 75 | late ValueNotifier state; 76 | late HookElement element; 77 | final hookWidget = HookBuilder( 78 | builder: (context) { 79 | element = context as HookElement; 80 | state = useState(0); 81 | return const SizedBox(); 82 | }, 83 | ); 84 | await tester.pumpWidget(hookWidget); 85 | 86 | expect( 87 | element.toStringDeep(), 88 | equalsIgnoringHashCodes( 89 | 'HookBuilder(useState: 0)\n' 90 | '└SizedBox(renderObject: RenderConstrainedBox#00000)\n', 91 | ), 92 | ); 93 | 94 | state.value++; 95 | 96 | await tester.pump(); 97 | 98 | expect( 99 | element.toStringDeep(), 100 | equalsIgnoringHashCodes( 101 | 'HookBuilder(useState: 1)\n' 102 | '└SizedBox(renderObject: RenderConstrainedBox#00000)\n', 103 | ), 104 | ); 105 | }); 106 | } 107 | -------------------------------------------------------------------------------- /packages/flutter_hooks/test/use_stream_controller_test.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:flutter/foundation.dart'; 4 | import 'package:flutter/widgets.dart'; 5 | import 'package:flutter_hooks/flutter_hooks.dart'; 6 | 7 | import 'mock.dart'; 8 | 9 | void main() { 10 | testWidgets('debugFillProperties', (tester) async { 11 | await tester.pumpWidget( 12 | HookBuilder(builder: (context) { 13 | useStreamController(); 14 | return const SizedBox(); 15 | }), 16 | ); 17 | 18 | await tester.pump(); 19 | 20 | final element = tester.element(find.byType(HookBuilder)); 21 | 22 | expect( 23 | element 24 | .toDiagnosticsNode(style: DiagnosticsTreeStyle.offstage) 25 | .toStringDeep(), 26 | equalsIgnoringHashCodes( 27 | 'HookBuilder\n' 28 | ' │ useStreamController: Instance of\n' 29 | // ignore: avoid_escaping_inner_quotes 30 | ' │ \'_AsyncBroadcastStreamController\'\n' 31 | ' └SizedBox(renderObject: RenderConstrainedBox#00000)\n', 32 | ), 33 | ); 34 | }); 35 | 36 | group('useStreamController', () { 37 | testWidgets('keys', (tester) async { 38 | late StreamController controller; 39 | 40 | await tester.pumpWidget(HookBuilder(builder: (context) { 41 | controller = useStreamController(); 42 | return Container(); 43 | })); 44 | 45 | final previous = controller; 46 | await tester.pumpWidget(HookBuilder(builder: (context) { 47 | controller = useStreamController(keys: []); 48 | return Container(); 49 | })); 50 | 51 | expect(previous, isNot(controller)); 52 | }); 53 | testWidgets('basics', (tester) async { 54 | late StreamController controller; 55 | 56 | await tester.pumpWidget(HookBuilder(builder: (context) { 57 | controller = useStreamController(); 58 | return Container(); 59 | })); 60 | 61 | expect( 62 | controller, 63 | isNot(isInstanceOf>()), 64 | ); 65 | expect(controller.onListen, isNull); 66 | expect(controller.onCancel, isNull); 67 | expect(() => controller.onPause, throwsUnsupportedError); 68 | expect(() => controller.onResume, throwsUnsupportedError); 69 | 70 | final previousController = controller; 71 | void onListen() {} 72 | void onCancel() {} 73 | await tester.pumpWidget(HookBuilder(builder: (context) { 74 | controller = useStreamController( 75 | sync: true, 76 | onCancel: onCancel, 77 | onListen: onListen, 78 | ); 79 | return Container(); 80 | })); 81 | 82 | expect(controller, previousController); 83 | expect( 84 | controller, 85 | isNot(isInstanceOf>()), 86 | ); 87 | expect(controller.onListen, onListen); 88 | expect(controller.onCancel, onCancel); 89 | expect(() => controller.onPause, throwsUnsupportedError); 90 | expect(() => controller.onResume, throwsUnsupportedError); 91 | 92 | await tester.pumpWidget(Container()); 93 | 94 | expect(controller.isClosed, true); 95 | }); 96 | testWidgets('sync', (tester) async { 97 | late StreamController controller; 98 | 99 | await tester.pumpWidget(HookBuilder(builder: (context) { 100 | controller = useStreamController(sync: true); 101 | return Container(); 102 | })); 103 | 104 | expect(controller, isInstanceOf>()); 105 | expect(controller.onListen, isNull); 106 | expect(controller.onCancel, isNull); 107 | expect(() => controller.onPause, throwsUnsupportedError); 108 | expect(() => controller.onResume, throwsUnsupportedError); 109 | 110 | final previousController = controller; 111 | void onListen() {} 112 | void onCancel() {} 113 | await tester.pumpWidget(HookBuilder(builder: (context) { 114 | controller = useStreamController( 115 | onCancel: onCancel, 116 | onListen: onListen, 117 | ); 118 | return Container(); 119 | })); 120 | 121 | expect(controller, previousController); 122 | expect(controller, isInstanceOf>()); 123 | expect(controller.onListen, onListen); 124 | expect(controller.onCancel, onCancel); 125 | expect(() => controller.onPause, throwsUnsupportedError); 126 | expect(() => controller.onResume, throwsUnsupportedError); 127 | 128 | await tester.pumpWidget(Container()); 129 | 130 | expect(controller.isClosed, true); 131 | }); 132 | }); 133 | } 134 | -------------------------------------------------------------------------------- /packages/flutter_hooks/test/use_tab_controller_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/foundation.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter/scheduler.dart'; 4 | import 'package:flutter_hooks/src/framework.dart'; 5 | import 'package:flutter_hooks/src/hooks.dart'; 6 | 7 | import 'mock.dart'; 8 | 9 | void main() { 10 | testWidgets('debugFillProperties', (tester) async { 11 | await tester.pumpWidget( 12 | HookBuilder(builder: (context) { 13 | useTabController(initialLength: 4); 14 | return const SizedBox(); 15 | }), 16 | ); 17 | 18 | await tester.pump(); 19 | 20 | final element = tester.element(find.byType(HookBuilder)); 21 | 22 | expect( 23 | element 24 | .toDiagnosticsNode(style: DiagnosticsTreeStyle.offstage) 25 | .toStringDeep(), 26 | equalsIgnoringHashCodes( 27 | 'HookBuilder\n' 28 | ' │ useSingleTickerProvider\n' 29 | " │ useTabController: Instance of 'TabController'\n" 30 | ' └SizedBox(renderObject: RenderConstrainedBox#00000)\n', 31 | ), 32 | ); 33 | }); 34 | 35 | group('useTabController', () { 36 | testWidgets('initial values matches with real constructor', (tester) async { 37 | late TabController controller; 38 | late TabController controller2; 39 | 40 | await tester.pumpWidget( 41 | HookBuilder(builder: (context) { 42 | final vsync = useSingleTickerProvider(); 43 | controller2 = TabController(length: 4, vsync: vsync); 44 | controller = useTabController(initialLength: 4); 45 | return Container(); 46 | }), 47 | ); 48 | 49 | expect(controller.index, controller2.index); 50 | }); 51 | testWidgets("returns a TabController that doesn't change", (tester) async { 52 | late TabController controller; 53 | late TabController controller2; 54 | 55 | await tester.pumpWidget( 56 | HookBuilder(builder: (context) { 57 | controller = useTabController(initialLength: 1); 58 | return Container(); 59 | }), 60 | ); 61 | 62 | expect(controller, isA()); 63 | 64 | await tester.pumpWidget( 65 | HookBuilder(builder: (context) { 66 | controller2 = useTabController(initialLength: 1); 67 | return Container(); 68 | }), 69 | ); 70 | 71 | expect(identical(controller, controller2), isTrue); 72 | }); 73 | testWidgets('changing length is no-op', (tester) async { 74 | late TabController controller; 75 | 76 | await tester.pumpWidget( 77 | HookBuilder(builder: (context) { 78 | controller = useTabController(initialLength: 1); 79 | return Container(); 80 | }), 81 | ); 82 | 83 | expect(controller.length, 1); 84 | 85 | await tester.pumpWidget( 86 | HookBuilder(builder: (context) { 87 | controller = useTabController(initialLength: 2); 88 | return Container(); 89 | }), 90 | ); 91 | 92 | expect(controller.length, 1); 93 | }); 94 | 95 | testWidgets('passes hook parameters to the TabController', (tester) async { 96 | late TabController controller; 97 | 98 | await tester.pumpWidget( 99 | HookBuilder( 100 | builder: (context) { 101 | controller = useTabController(initialIndex: 2, initialLength: 4); 102 | 103 | return Container(); 104 | }, 105 | ), 106 | ); 107 | 108 | expect(controller.index, 2); 109 | expect(controller.length, 4); 110 | }); 111 | testWidgets('allows passing custom vsync', (tester) async { 112 | final vsync = TickerProviderMock(); 113 | final ticker = Ticker((_) {}); 114 | when(vsync.createTicker((_) {})).thenReturn(ticker); 115 | 116 | await tester.pumpWidget( 117 | HookBuilder( 118 | builder: (context) { 119 | useTabController(initialLength: 1, vsync: vsync); 120 | 121 | return Container(); 122 | }, 123 | ), 124 | ); 125 | 126 | verify(vsync.createTicker((_) {})).called(1); 127 | verifyNoMoreInteractions(vsync); 128 | 129 | await tester.pumpWidget( 130 | HookBuilder( 131 | builder: (context) { 132 | useTabController(initialLength: 1, vsync: vsync); 133 | return Container(); 134 | }, 135 | ), 136 | ); 137 | 138 | verifyNoMoreInteractions(vsync); 139 | ticker.dispose(); 140 | }); 141 | 142 | testWidgets('initial animationDuration matches with real constructor', 143 | (tester) async { 144 | late TabController controller; 145 | late TabController controller2; 146 | 147 | await tester.pumpWidget( 148 | HookBuilder( 149 | builder: (context) { 150 | final vsync = useSingleTickerProvider(); 151 | controller = useTabController(initialLength: 4); 152 | controller2 = TabController(length: 4, vsync: vsync); 153 | return Container(); 154 | }, 155 | ), 156 | ); 157 | 158 | expect(controller.animationDuration, controller2.animationDuration); 159 | }); 160 | }); 161 | } 162 | 163 | class TickerProviderMock extends Mock implements TickerProvider { 164 | @override 165 | Ticker createTicker(TickerCallback onTick) => super.noSuchMethod( 166 | Invocation.getter(#createTicker), 167 | returnValue: Ticker(onTick), 168 | ) as Ticker; 169 | } 170 | -------------------------------------------------------------------------------- /packages/flutter_hooks/test/use_text_editing_controller_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/foundation.dart'; 2 | import 'package:flutter/widgets.dart'; 3 | import 'package:flutter_hooks/src/framework.dart'; 4 | import 'package:flutter_hooks/src/hooks.dart'; 5 | import 'package:flutter_test/flutter_test.dart'; 6 | 7 | import 'mock.dart'; 8 | 9 | void main() { 10 | testWidgets('debugFillProperties', (tester) async { 11 | await tester.pumpWidget( 12 | HookBuilder(builder: (context) { 13 | useTextEditingController(); 14 | return const SizedBox(); 15 | }), 16 | ); 17 | 18 | await tester.pump(); 19 | 20 | final element = tester.element(find.byType(HookBuilder)); 21 | 22 | expect( 23 | element 24 | .toDiagnosticsNode(style: DiagnosticsTreeStyle.offstage) 25 | .toStringDeep(), 26 | equalsIgnoringHashCodes( 27 | 'HookBuilder\n' 28 | ' │ useTextEditingController:\n' 29 | ' │ TextEditingController#00000(TextEditingValue(text: ┤├,\n' 30 | ' │ selection: TextSelection.invalid, composing: TextRange(start:\n' 31 | ' │ -1, end: -1)))\n' 32 | ' └SizedBox(renderObject: RenderConstrainedBox#00000)\n', 33 | ), 34 | ); 35 | }); 36 | 37 | testWidgets('useTextEditingController returns a controller', (tester) async { 38 | final rebuilder = ValueNotifier(0); 39 | late TextEditingController controller; 40 | 41 | await tester.pumpWidget(HookBuilder( 42 | builder: (context) { 43 | controller = useTextEditingController(); 44 | useValueListenable(rebuilder); 45 | return Container(); 46 | }, 47 | )); 48 | 49 | expect(controller, isNotNull); 50 | controller.addListener(() {}); 51 | 52 | // rebuild hook 53 | final firstController = controller; 54 | rebuilder.notifyListeners(); 55 | await tester.pumpAndSettle(); 56 | expect(identical(controller, firstController), isTrue, 57 | reason: 'Controllers should be identical after rebuilds'); 58 | 59 | // pump another widget so that the old one gets disposed 60 | await tester.pumpWidget(Container()); 61 | 62 | expect( 63 | () => controller.addListener(() {}), 64 | throwsA(isFlutterError.having( 65 | (e) => e.message, 'message', contains('disposed'))), 66 | ); 67 | }); 68 | 69 | testWidgets('respects initial text property', (tester) async { 70 | final rebuilder = ValueNotifier(0); 71 | late TextEditingController controller; 72 | const initialText = 'hello hooks'; 73 | var targetText = initialText; 74 | 75 | await tester.pumpWidget(HookBuilder( 76 | builder: (context) { 77 | controller = useTextEditingController(text: targetText); 78 | useValueListenable(rebuilder); 79 | return Container(); 80 | }, 81 | )); 82 | 83 | expect(controller.text, targetText); 84 | 85 | // change text and rebuild - the value of the controller shouldn't change 86 | targetText = "can't see me!"; 87 | rebuilder.notifyListeners(); 88 | await tester.pumpAndSettle(); 89 | expect(controller.text, initialText); 90 | }); 91 | 92 | testWidgets('respects initial value property', (tester) async { 93 | final rebuilder = ValueNotifier(0); 94 | const initialValue = TextEditingValue( 95 | text: 'foo', 96 | selection: TextSelection.collapsed(offset: 2), 97 | ); 98 | var targetValue = initialValue; 99 | late TextEditingController controller; 100 | 101 | await tester.pumpWidget(HookBuilder( 102 | builder: (context) { 103 | controller = useTextEditingController.fromValue(targetValue); 104 | useValueListenable(rebuilder); 105 | return Container(); 106 | }, 107 | )); 108 | 109 | expect(controller.value, targetValue); 110 | 111 | // similar to above - the value should not change after a rebuild 112 | targetValue = const TextEditingValue(text: 'another'); 113 | rebuilder.notifyListeners(); 114 | await tester.pumpAndSettle(); 115 | expect(controller.value, initialValue); 116 | }); 117 | } 118 | -------------------------------------------------------------------------------- /packages/flutter_hooks/test/use_ticker_provider_test.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:flutter/foundation.dart'; 4 | import 'package:flutter/widgets.dart'; 5 | import 'package:flutter_hooks/flutter_hooks.dart'; 6 | 7 | import 'mock.dart'; 8 | 9 | void main() { 10 | testWidgets('debugFillProperties', (tester) async { 11 | await tester.pumpWidget( 12 | HookBuilder(builder: (context) { 13 | useSingleTickerProvider(); 14 | return const SizedBox(); 15 | }), 16 | ); 17 | 18 | await tester.pump(); 19 | 20 | final element = tester.element(find.byType(HookBuilder)); 21 | 22 | expect( 23 | element 24 | .toDiagnosticsNode(style: DiagnosticsTreeStyle.offstage) 25 | .toStringDeep(), 26 | equalsIgnoringHashCodes( 27 | 'HookBuilder\n' 28 | ' │ useSingleTickerProvider\n' 29 | ' └SizedBox(renderObject: RenderConstrainedBox#00000)\n', 30 | ), 31 | ); 32 | }); 33 | 34 | testWidgets('useSingleTickerProvider basic', (tester) async { 35 | late TickerProvider provider; 36 | 37 | await tester.pumpWidget(TickerMode( 38 | enabled: true, 39 | child: HookBuilder(builder: (context) { 40 | provider = useSingleTickerProvider(); 41 | return Container(); 42 | }), 43 | )); 44 | 45 | final animationController = AnimationController( 46 | vsync: provider, 47 | duration: const Duration(seconds: 1), 48 | ); 49 | unawaited(animationController.forward()); 50 | 51 | expect(() => AnimationController(vsync: provider), throwsFlutterError); 52 | 53 | animationController.dispose(); 54 | 55 | await tester.pumpWidget(const SizedBox()); 56 | }); 57 | 58 | testWidgets('useSingleTickerProvider unused', (tester) async { 59 | await tester.pumpWidget(HookBuilder(builder: (context) { 60 | useSingleTickerProvider(); 61 | return Container(); 62 | })); 63 | 64 | await tester.pumpWidget(const SizedBox()); 65 | }); 66 | 67 | testWidgets('useSingleTickerProvider still active', (tester) async { 68 | late TickerProvider provider; 69 | 70 | await tester.pumpWidget(TickerMode( 71 | enabled: true, 72 | child: HookBuilder(builder: (context) { 73 | provider = useSingleTickerProvider(); 74 | return Container(); 75 | }), 76 | )); 77 | 78 | final animationController = AnimationController( 79 | vsync: provider, 80 | duration: const Duration(seconds: 1), 81 | ); 82 | 83 | try { 84 | // ignore: unawaited_futures 85 | animationController.forward(); 86 | 87 | await tester.pumpWidget(const SizedBox()); 88 | 89 | expect(tester.takeException(), isFlutterError); 90 | } finally { 91 | animationController.dispose(); 92 | } 93 | }); 94 | 95 | testWidgets('useSingleTickerProvider pass down keys', (tester) async { 96 | late TickerProvider provider; 97 | List? keys; 98 | 99 | await tester.pumpWidget(HookBuilder(builder: (context) { 100 | provider = useSingleTickerProvider(keys: keys); 101 | return Container(); 102 | })); 103 | 104 | final previousProvider = provider; 105 | keys = []; 106 | 107 | await tester.pumpWidget(HookBuilder(builder: (context) { 108 | provider = useSingleTickerProvider(keys: keys); 109 | return Container(); 110 | })); 111 | 112 | expect(previousProvider, isNot(provider)); 113 | }); 114 | } 115 | -------------------------------------------------------------------------------- /packages/flutter_hooks/test/use_transformation_controller_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/foundation.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter_hooks/src/framework.dart'; 4 | import 'package:flutter_hooks/src/hooks.dart'; 5 | 6 | import 'mock.dart'; 7 | 8 | void main() { 9 | testWidgets('debugFillProperties', (tester) async { 10 | await tester.pumpWidget( 11 | HookBuilder(builder: (context) { 12 | useTransformationController(); 13 | return const SizedBox(); 14 | }), 15 | ); 16 | 17 | final element = tester.element(find.byType(HookBuilder)); 18 | 19 | expect( 20 | element 21 | .toDiagnosticsNode(style: DiagnosticsTreeStyle.offstage) 22 | .toStringDeep(), 23 | anyOf( 24 | equalsIgnoringHashCodes( 25 | 'HookBuilder\n' 26 | ' │ useTransformationController:\n' 27 | ' │ TransformationController#00000([0] 1.0,0.0,0.0,0.0\n' 28 | ' │ [1] 0.0,1.0,0.0,0.0\n' 29 | ' │ [2] 0.0,0.0,1.0,0.0\n' 30 | ' │ [3] 0.0,0.0,0.0,1.0\n' 31 | ' │ )\n' 32 | ' └SizedBox(renderObject: RenderConstrainedBox#00000)\n', 33 | ), 34 | equalsIgnoringHashCodes( 35 | 'HookBuilder\n' 36 | ' │ useTransformationController:\n' 37 | ' │ TransformationController#00000([0] [1.0,0.0,0.0,0.0]\n' 38 | ' │ [1] [0.0,1.0,0.0,0.0]\n' 39 | ' │ [2] [0.0,0.0,1.0,0.0]\n' 40 | ' │ [3] [0.0,0.0,0.0,1.0]\n' 41 | ' │ )\n' 42 | ' └SizedBox(renderObject: RenderConstrainedBox#00000)\n', 43 | ), 44 | ), 45 | ); 46 | }); 47 | 48 | group('useTransformationController', () { 49 | testWidgets('initial values matches with real constructor', (tester) async { 50 | late TransformationController controller; 51 | late TransformationController controller2; 52 | 53 | await tester.pumpWidget( 54 | HookBuilder(builder: (context) { 55 | controller2 = TransformationController(); 56 | controller = useTransformationController(); 57 | return Container(); 58 | }), 59 | ); 60 | 61 | expect(controller.value, controller2.value); 62 | }); 63 | testWidgets("returns a TransformationController that doesn't change", 64 | (tester) async { 65 | late TransformationController controller; 66 | late TransformationController controller2; 67 | 68 | await tester.pumpWidget( 69 | HookBuilder(builder: (context) { 70 | controller = useTransformationController(); 71 | return Container(); 72 | }), 73 | ); 74 | 75 | expect(controller, isA()); 76 | 77 | await tester.pumpWidget( 78 | HookBuilder(builder: (context) { 79 | controller2 = useTransformationController(); 80 | return Container(); 81 | }), 82 | ); 83 | 84 | expect(identical(controller, controller2), isTrue); 85 | }); 86 | 87 | testWidgets('passes hook parameters to the TransformationController', 88 | (tester) async { 89 | late TransformationController controller; 90 | 91 | await tester.pumpWidget( 92 | HookBuilder( 93 | builder: (context) { 94 | controller = useTransformationController( 95 | initialValue: Matrix4.translationValues(1, 2, 3), 96 | ); 97 | 98 | return Container(); 99 | }, 100 | ), 101 | ); 102 | 103 | expect(controller.value, Matrix4.translationValues(1, 2, 3)); 104 | }); 105 | }); 106 | } 107 | 108 | class TickerProviderMock extends Mock implements TickerProvider {} 109 | -------------------------------------------------------------------------------- /packages/flutter_hooks/test/use_tree_sliver_controller_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/foundation.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter_hooks/src/framework.dart'; 4 | import 'package:flutter_hooks/src/hooks.dart'; 5 | 6 | import 'mock.dart'; 7 | 8 | void main() { 9 | testWidgets('debugFillProperties', (tester) async { 10 | await tester.pumpWidget( 11 | HookBuilder(builder: (context) { 12 | useTreeSliverController(); 13 | return const SizedBox(); 14 | }), 15 | ); 16 | 17 | await tester.pump(); 18 | 19 | final element = tester.element(find.byType(HookBuilder)); 20 | 21 | expect( 22 | element 23 | .toDiagnosticsNode(style: DiagnosticsTreeStyle.offstage) 24 | .toStringDeep(), 25 | equalsIgnoringHashCodes( 26 | 'HookBuilder\n' 27 | " │ useTreeSliverController: Instance of 'TreeSliverController'\n" 28 | ' └SizedBox(renderObject: RenderConstrainedBox#00000)\n', 29 | ), 30 | ); 31 | }); 32 | 33 | group('useTreeSliverController', () { 34 | testWidgets('check expansion/collapse of node', (tester) async { 35 | late TreeSliverController controller; 36 | final tree = >[ 37 | TreeSliverNode(0, children: [TreeSliverNode(1), TreeSliverNode(2)]), 38 | TreeSliverNode( 39 | expanded: true, 3, children: [TreeSliverNode(4), TreeSliverNode(5)]) 40 | ]; 41 | await tester.pumpWidget(MaterialApp( 42 | home: Scaffold( 43 | body: HookBuilder(builder: (context) { 44 | controller = useTreeSliverController(); 45 | return CustomScrollView(slivers: [ 46 | TreeSliver( 47 | controller: controller, 48 | tree: tree, 49 | ), 50 | ]); 51 | }), 52 | ), 53 | )); 54 | 55 | expect(controller.isExpanded(tree[0]), false); 56 | controller.expandNode(tree[0]); 57 | expect(controller.isExpanded(tree[0]), true); 58 | controller.collapseNode(tree[0]); 59 | expect(controller.isExpanded(tree[0]), false); 60 | 61 | expect(controller.isExpanded(tree[1]), true); 62 | controller.collapseNode(tree[1]); 63 | expect(controller.isExpanded(tree[1]), false); 64 | controller.expandNode(tree[1]); 65 | expect(controller.isExpanded(tree[1]), true); 66 | }); 67 | }); 68 | } 69 | -------------------------------------------------------------------------------- /packages/flutter_hooks/test/use_value_changed_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/foundation.dart'; 2 | import 'package:flutter/widgets.dart'; 3 | import 'package:flutter_hooks/flutter_hooks.dart'; 4 | 5 | import 'mock.dart'; 6 | 7 | void main() { 8 | testWidgets('diagnostics', (tester) async { 9 | await tester.pumpWidget( 10 | HookBuilder(builder: (context) { 11 | useValueChanged(0, (_, __) => 21); 12 | return const SizedBox(); 13 | }), 14 | ); 15 | 16 | await tester.pumpWidget( 17 | HookBuilder(builder: (context) { 18 | useValueChanged(42, (_, __) => 21); 19 | return const SizedBox(); 20 | }), 21 | ); 22 | 23 | final element = tester.element(find.byType(HookBuilder)); 24 | 25 | expect( 26 | element 27 | .toDiagnosticsNode(style: DiagnosticsTreeStyle.offstage) 28 | .toStringDeep(), 29 | equalsIgnoringHashCodes( 30 | 'HookBuilder\n' 31 | ' │ useValueChanged: _ValueChangedHookState#00000(21,\n' 32 | ' │ value: 42, result: 21)\n' 33 | ' └SizedBox(renderObject: RenderConstrainedBox#00000)\n', 34 | ), 35 | ); 36 | }); 37 | 38 | testWidgets('useValueChanged basic', (tester) async { 39 | var value = 42; 40 | final _useValueChanged = MockValueChanged(); 41 | late String? result; 42 | 43 | Future pump() { 44 | return tester.pumpWidget( 45 | HookBuilder(builder: (context) { 46 | result = useValueChanged(value, _useValueChanged); 47 | return Container(); 48 | }), 49 | ); 50 | } 51 | 52 | await pump(); 53 | 54 | final context = find.byType(HookBuilder).evaluate().first; 55 | 56 | expect(result, null); 57 | verifyNoMoreInteractions(_useValueChanged); 58 | expect(context.dirty, false); 59 | 60 | await pump(); 61 | 62 | expect(result, null); 63 | verifyNoMoreInteractions(_useValueChanged); 64 | expect(context.dirty, false); 65 | 66 | value++; 67 | when(_useValueChanged(any, any)).thenReturn('Hello'); 68 | await pump(); 69 | 70 | verify(_useValueChanged(42, null)); 71 | expect(result, 'Hello'); 72 | verifyNoMoreInteractions(_useValueChanged); 73 | expect(context.dirty, false); 74 | 75 | await pump(); 76 | 77 | expect(result, 'Hello'); 78 | verifyNoMoreInteractions(_useValueChanged); 79 | expect(context.dirty, false); 80 | 81 | value++; 82 | when(_useValueChanged(any, any)).thenReturn('Foo'); 83 | await pump(); 84 | 85 | expect(result, 'Foo'); 86 | verify(_useValueChanged(43, 'Hello')); 87 | verifyNoMoreInteractions(_useValueChanged); 88 | expect(context.dirty, false); 89 | 90 | await pump(); 91 | 92 | expect(result, 'Foo'); 93 | verifyNoMoreInteractions(_useValueChanged); 94 | expect(context.dirty, false); 95 | 96 | // dispose 97 | await tester.pumpWidget(const SizedBox()); 98 | }); 99 | } 100 | 101 | class MockValueChanged extends Mock { 102 | String? call(int? value, String? previous); 103 | } 104 | -------------------------------------------------------------------------------- /packages/flutter_hooks/test/use_value_listenable_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/foundation.dart'; 2 | import 'package:flutter/widgets.dart'; 3 | import 'package:flutter_hooks/flutter_hooks.dart'; 4 | 5 | import 'mock.dart'; 6 | 7 | void main() { 8 | testWidgets('diagnostics', (tester) async { 9 | await tester.pumpWidget( 10 | HookBuilder(builder: (context) { 11 | useValueListenable(ValueNotifier(0)); 12 | return const SizedBox(); 13 | }), 14 | ); 15 | 16 | final element = tester.element(find.byType(HookBuilder)); 17 | 18 | expect( 19 | element 20 | .toDiagnosticsNode(style: DiagnosticsTreeStyle.offstage) 21 | .toStringDeep(), 22 | equalsIgnoringHashCodes( 23 | 'HookBuilder\n' 24 | ' │ useValueListenable: 0\n' 25 | ' └SizedBox(renderObject: RenderConstrainedBox#00000)\n', 26 | ), 27 | ); 28 | }); 29 | testWidgets('useValueListenable', (tester) async { 30 | var listenable = ValueNotifier(0); 31 | late int result; 32 | 33 | Future pump() { 34 | return tester.pumpWidget(HookBuilder( 35 | builder: (context) { 36 | result = useValueListenable(listenable); 37 | return Container(); 38 | }, 39 | )); 40 | } 41 | 42 | await pump(); 43 | 44 | final element = tester.firstElement(find.byType(HookBuilder)); 45 | 46 | expect(result, 0); 47 | // ignore: invalid_use_of_protected_member 48 | expect(listenable.hasListeners, true); 49 | expect(element.dirty, false); 50 | listenable.value++; 51 | expect(element.dirty, true); 52 | await tester.pump(); 53 | expect(result, 1); 54 | expect(element.dirty, false); 55 | 56 | final previousListenable = listenable; 57 | listenable = ValueNotifier(0); 58 | 59 | await pump(); 60 | 61 | expect(result, 0); 62 | // ignore: invalid_use_of_protected_member 63 | expect(previousListenable.hasListeners, false); 64 | // ignore: invalid_use_of_protected_member 65 | expect(listenable.hasListeners, true); 66 | expect(element.dirty, false); 67 | listenable.value++; 68 | expect(element.dirty, true); 69 | await tester.pump(); 70 | expect(result, 1); 71 | expect(element.dirty, false); 72 | 73 | await tester.pumpWidget(const SizedBox()); 74 | 75 | // ignore: invalid_use_of_protected_member 76 | expect(listenable.hasListeners, false); 77 | 78 | listenable.dispose(); 79 | previousListenable.dispose(); 80 | }); 81 | } 82 | -------------------------------------------------------------------------------- /packages/flutter_hooks/test/use_value_notifier_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/foundation.dart'; 2 | import 'package:flutter/widgets.dart'; 3 | import 'package:flutter_hooks/flutter_hooks.dart'; 4 | 5 | import 'mock.dart'; 6 | 7 | void main() { 8 | testWidgets('diagnostics', (tester) async { 9 | await tester.pumpWidget( 10 | HookBuilder(builder: (context) { 11 | useValueNotifier(0); 12 | return const SizedBox(); 13 | }), 14 | ); 15 | 16 | final element = tester.element(find.byType(HookBuilder)); 17 | 18 | expect( 19 | element 20 | .toDiagnosticsNode(style: DiagnosticsTreeStyle.offstage) 21 | .toStringDeep(), 22 | equalsIgnoringHashCodes( 23 | 'HookBuilder\n' 24 | ' │ useValueNotifier: ValueNotifier#00000(0)\n' 25 | ' └SizedBox(renderObject: RenderConstrainedBox#00000)\n', 26 | ), 27 | ); 28 | }); 29 | 30 | group('useValueNotifier', () { 31 | testWidgets('useValueNotifier basic', (tester) async { 32 | late ValueNotifier state; 33 | late HookElement element; 34 | final listener = MockListener(); 35 | 36 | await tester.pumpWidget(HookBuilder( 37 | builder: (context) { 38 | element = context as HookElement; 39 | state = useValueNotifier(42); 40 | return Container(); 41 | }, 42 | )); 43 | 44 | state.addListener(listener); 45 | 46 | expect(state.value, 42); 47 | expect(element.dirty, false); 48 | verifyNoMoreInteractions(listener); 49 | 50 | await tester.pump(); 51 | 52 | verifyNoMoreInteractions(listener); 53 | expect(state.value, 42); 54 | expect(element.dirty, false); 55 | 56 | state.value++; 57 | verify(listener()).called(1); 58 | verifyNoMoreInteractions(listener); 59 | expect(element.dirty, false); 60 | await tester.pump(); 61 | 62 | expect(state.value, 43); 63 | expect(element.dirty, false); 64 | verifyNoMoreInteractions(listener); 65 | 66 | // dispose 67 | await tester.pumpWidget(const SizedBox()); 68 | 69 | expect(() => state.addListener(() {}), throwsFlutterError); 70 | }); 71 | 72 | testWidgets('no initial data', (tester) async { 73 | late ValueNotifier state; 74 | late HookElement element; 75 | final listener = MockListener(); 76 | 77 | await tester.pumpWidget(HookBuilder( 78 | builder: (context) { 79 | element = context as HookElement; 80 | state = useValueNotifier(null); 81 | return Container(); 82 | }, 83 | )); 84 | 85 | state.addListener(listener); 86 | 87 | expect(state.value, null); 88 | expect(element.dirty, false); 89 | verifyNoMoreInteractions(listener); 90 | 91 | await tester.pump(); 92 | 93 | expect(state.value, null); 94 | expect(element.dirty, false); 95 | verifyNoMoreInteractions(listener); 96 | 97 | state.value = 43; 98 | expect(element.dirty, false); 99 | verify(listener()).called(1); 100 | verifyNoMoreInteractions(listener); 101 | await tester.pump(); 102 | 103 | expect(state.value, 43); 104 | expect(element.dirty, false); 105 | verifyNoMoreInteractions(listener); 106 | 107 | // dispose 108 | await tester.pumpWidget(const SizedBox()); 109 | 110 | expect(() => state.addListener(() {}), throwsFlutterError); 111 | }); 112 | 113 | testWidgets('creates new valuenotifier when key change', (tester) async { 114 | late ValueNotifier state; 115 | late ValueNotifier previous; 116 | 117 | await tester.pumpWidget(HookBuilder( 118 | builder: (context) { 119 | state = useValueNotifier(42); 120 | return Container(); 121 | }, 122 | )); 123 | 124 | await tester.pumpWidget(HookBuilder( 125 | builder: (context) { 126 | previous = state; 127 | state = useValueNotifier(42, [42]); 128 | return Container(); 129 | }, 130 | )); 131 | 132 | expect(state, isNot(previous)); 133 | }); 134 | testWidgets("instance stays the same when keys don't change", 135 | (tester) async { 136 | late ValueNotifier state; 137 | late ValueNotifier previous; 138 | 139 | await tester.pumpWidget(HookBuilder( 140 | builder: (context) { 141 | state = useValueNotifier(0, [42]); 142 | return Container(); 143 | }, 144 | )); 145 | 146 | await tester.pumpWidget(HookBuilder( 147 | builder: (context) { 148 | previous = state; 149 | state = useValueNotifier(42, [42]); 150 | return Container(); 151 | }, 152 | )); 153 | 154 | expect(state, previous); 155 | }); 156 | }); 157 | } 158 | 159 | class MockListener extends Mock { 160 | void call(); 161 | } 162 | --------------------------------------------------------------------------------