From 5e28ef8ea57940adc4d08e7925dcd3eff07e3753 Mon Sep 17 00:00:00 2001 From: JohnE Date: Tue, 11 Jun 2019 13:55:39 -0700 Subject: [PATCH] NEW: added provider plugin --- packages/provider/.travis.yml | 27 + packages/provider/README.md | 182 ++++++ packages/provider/analysis_options.yaml | 82 +++ .../provider/packages/provider/.gitignore | 14 + .../provider/packages/provider/CHANGELOG.md | 79 +++ packages/provider/packages/provider/LICENSE | 21 + packages/provider/packages/provider/README.md | 182 ++++++ .../packages/provider/example/.gitignore | 70 +++ .../packages/provider/example/.metadata | 10 + .../packages/provider/example/lib/main.dart | 125 ++++ .../packages/provider/example/pubspec.yaml | 23 + .../provider/example/test/widget_test.dart | 35 ++ .../packages/provider/lib/provider.dart | 11 + .../provider/lib/src/async_provider.dart | 306 ++++++++++ .../lib/src/change_notifier_provider.dart | 223 +++++++ .../packages/provider/lib/src/consumer.dart | 212 +++++++ .../provider/lib/src/delegate_widget.dart | 279 +++++++++ .../provider/lib/src/listenable_provider.dart | 355 +++++++++++ .../packages/provider/lib/src/provider.dart | 347 +++++++++++ .../provider/lib/src/proxy_provider.dart | 437 ++++++++++++++ .../lib/src/value_listenable_provider.dart | 106 ++++ .../provider/packages/provider/pubspec.yaml | 30 + .../test/change_notifier_provider_test.dart | 336 +++++++++++ .../change_notifier_proxy_provider_test.dart | 258 ++++++++ .../packages/provider/test/common.dart | 106 ++++ .../packages/provider/test/consumer_test.dart | 215 +++++++ .../provider/test/delegate_widget_test.dart | 361 ++++++++++++ .../provider/test/future_provider_test.dart | 325 +++++++++++ .../test/listenable_provider_test.dart | 375 ++++++++++++ .../test/listenable_proxy_provider_test.dart | 246 ++++++++ .../provider/test/multi_provider_test.dart | 111 ++++ .../packages/provider/test/provider_test.dart | 225 +++++++ .../provider/test/proxy_provider_test.dart | 552 ++++++++++++++++++ .../packages/provider/test/readme_test.dart | 17 + .../provider/test/stateful_provider_test.dart | 74 +++ .../provider/test/stream_provider_test.dart | 377 ++++++++++++ .../test/value_listenable_provider_test.dart | 162 +++++ packages/provider/scripts/flutter_test.sh | 7 + 38 files changed, 6903 insertions(+) create mode 100644 packages/provider/.travis.yml create mode 100644 packages/provider/README.md create mode 100644 packages/provider/analysis_options.yaml create mode 100644 packages/provider/packages/provider/.gitignore create mode 100644 packages/provider/packages/provider/CHANGELOG.md create mode 100644 packages/provider/packages/provider/LICENSE create mode 100644 packages/provider/packages/provider/README.md create mode 100644 packages/provider/packages/provider/example/.gitignore create mode 100644 packages/provider/packages/provider/example/.metadata create mode 100644 packages/provider/packages/provider/example/lib/main.dart create mode 100644 packages/provider/packages/provider/example/pubspec.yaml create mode 100644 packages/provider/packages/provider/example/test/widget_test.dart create mode 100644 packages/provider/packages/provider/lib/provider.dart create mode 100644 packages/provider/packages/provider/lib/src/async_provider.dart create mode 100644 packages/provider/packages/provider/lib/src/change_notifier_provider.dart create mode 100644 packages/provider/packages/provider/lib/src/consumer.dart create mode 100644 packages/provider/packages/provider/lib/src/delegate_widget.dart create mode 100644 packages/provider/packages/provider/lib/src/listenable_provider.dart create mode 100644 packages/provider/packages/provider/lib/src/provider.dart create mode 100644 packages/provider/packages/provider/lib/src/proxy_provider.dart create mode 100644 packages/provider/packages/provider/lib/src/value_listenable_provider.dart create mode 100644 packages/provider/packages/provider/pubspec.yaml create mode 100644 packages/provider/packages/provider/test/change_notifier_provider_test.dart create mode 100644 packages/provider/packages/provider/test/change_notifier_proxy_provider_test.dart create mode 100644 packages/provider/packages/provider/test/common.dart create mode 100644 packages/provider/packages/provider/test/consumer_test.dart create mode 100644 packages/provider/packages/provider/test/delegate_widget_test.dart create mode 100644 packages/provider/packages/provider/test/future_provider_test.dart create mode 100644 packages/provider/packages/provider/test/listenable_provider_test.dart create mode 100644 packages/provider/packages/provider/test/listenable_proxy_provider_test.dart create mode 100644 packages/provider/packages/provider/test/multi_provider_test.dart create mode 100644 packages/provider/packages/provider/test/provider_test.dart create mode 100644 packages/provider/packages/provider/test/proxy_provider_test.dart create mode 100644 packages/provider/packages/provider/test/readme_test.dart create mode 100644 packages/provider/packages/provider/test/stateful_provider_test.dart create mode 100644 packages/provider/packages/provider/test/stream_provider_test.dart create mode 100644 packages/provider/packages/provider/test/value_listenable_provider_test.dart create mode 100755 packages/provider/scripts/flutter_test.sh diff --git a/packages/provider/.travis.yml b/packages/provider/.travis.yml new file mode 100644 index 0000000..f2ee26f --- /dev/null +++ b/packages/provider/.travis.yml @@ -0,0 +1,27 @@ +language: bash +os: + - osx +env: + - FLUTTER_CHANNEL="stable" + - FLUTTER_CHANNEL="master" +sudo: false +before_script: + - cd .. + - git clone https://github.com/flutter/flutter.git -b $FLUTTER_CHANNEL + - export PATH=$PATH:$PWD/flutter/bin:$PWD/flutter/bin/cache/dart-sdk/bin + - cd - + - flutter doctor +script: + - set -e # abort CI if an error happens + - ./scripts/flutter_test.sh packages/provider + - ./scripts/flutter_test.sh packages/provider/example + + # export coverage + - if [ $FLUTTER_CHANNEL = "stable" ]; then + bash <(curl -s https://codecov.io/bash); + fi +matrix: + fast_finish: true +cache: + directories: + - $HOME/.pub-cache diff --git a/packages/provider/README.md b/packages/provider/README.md new file mode 100644 index 0000000..f9c2963 --- /dev/null +++ b/packages/provider/README.md @@ -0,0 +1,182 @@ +[![Build Status](https://travis-ci.org/rrousselGit/provider.svg?branch=master)](https://travis-ci.org/rrousselGit/provider) +[![pub package](https://img.shields.io/pub/v/provider.svg)](https://pub.dartlang.org/packages/provider) [![codecov](https://codecov.io/gh/rrousselGit/provider/branch/master/graph/badge.svg)](https://codecov.io/gh/rrousselGit/provider) [![Gitter](https://badges.gitter.im/flutter_provider/community.svg)](https://gitter.im/flutter_provider/community?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) + +A dependency injection system built with widgets for widgets. `provider` is mostly syntax sugar for `InheritedWidget`, +to make common use-cases straightforward. + +## Migration from v2.0.0 to v3.0.0 + +- Providers can no longer be instantiated with `const`. +- `Provider` now throws if used with a `Listenable`/`Stream`. + Consider using `ListenableProvider`/`StreamProvider` instead. Alternatively, + this exception can be disabled by setting `Provider.debugCheckInvalidValueType` + to `null` like so: + +```dart +void main() { + Provider.debugCheckInvalidValueType = null; + + runApp(MyApp()); +} +``` + + +- All `XXProvider.value` constructors now use `value` as parameter name. + +Before: + +```dart +ChangeNotifierProvider.value(notifier: myNotifier), +``` + +After: + +```dart +ChangeNotifierProvider.value(value: myNotifier), +``` + +- `StreamProvider`'s default constructor now builds a `Stream` instead of `StreamController`. The previous behavior has been moved to the named constructor `StreamProvider.controller`. + +Before: + +```dart +StreamProvider(builder: (_) => StreamController()), +``` + +After: + +```dart +StreamProvider.controller(builder: (_) => StreamController()), +``` + +## Usage + +### Exposing a value + +To expose a variable using `provider`, wrap any widget into one of the provider widgets from this package +and pass it your variable. Then, all descendants of the newly added provider widget can access this variable. + +A simple example would be to wrap the entire application into a `Provider` widget and pass it our variable: + +```dart +Provider.value( + value: 'Hello World', + child: MaterialApp( + home: Home(), + ) +) +``` + +Alternatively, for complex objects, most providers expose a constructor that takes a function to create the value. +The provider will call that function only once, when inserting the widget in the tree, and expose the result. +This is perfect for exposing a complex object that never changes over time without writing a `StatefulWidget`. + +The following creates and exposes a `MyComplexClass`. And in the event where `Provider` is removed from the widget tree, +the instantiated `MyComplexClass` will be disposed. + +```dart +Provider( + builder: (context) => MyComplexClass(), + dispose: (context, value) => value.dispose() + child: SomeWidget(), +) +``` + +### Reading a value + +The easiest way to read a value is by using the static method `Provider.of(BuildContext context)`. This method will look +up in widget tree starting from the widget associated with the `BuildContext` passed and it will return the nearest variable +of type `T` found (or throw if nothing if found). + +Combined with the first example of [exposing a value](#exposing-a-value), this widget will read the exposed `String` and render "Hello World." + +```dart +class Home extends StatelessWidget { + @override + Widget build(BuildContext context) { + return Text( + /// Don't forget to pass the type of the object you want to obtain to `Provider.of`! + Provider.of(context) + ); + } +} +``` + +Alternatively instead of using `Provider.of`, we can use the `Consumer` widget. + +This can be useful for performance optimizations or when it is difficult to obtain a `BuildContext` descendant of the provider. + +```dart +Provider.value( + value: 'Hello World', + child: Consumer( + builder: (context, value, child) => Text(value), + ), +); +``` + +--- + +Note that you can freely use multiple providers with different types together: + +```dart +Provider.value( + value: 42, + child: Provider.value( + value: 'Hello World', + child: // ... + ) +) +``` + +And obtain their value independently: + +```dart +var value = Provider.of(context); +var value2 = Provider.of(context); +``` + +### MultiProvider + +When injecting many values in big applications, `Provider` can rapidly become pretty nested: + +```dart +Provider.value( + value: foo, + child: Provider.value( + value: bar, + child: Provider.value( + value: baz, + child: someWidget, + ) + ) +) +``` + +In that situation, we can use `MultiProvider` to improve the readability: + +```dart +MultiProvider( + providers: [ + Provider.value(value: foo), + Provider.value(value: bar), + Provider.value(value: baz), + ], + child: someWidget, +) +``` + +The behavior of both examples is strictly the same. `MultiProvider` only changes the appearance of the code. + +### Existing providers + +`provider` exposes a few different kinds of "provider" for different types of objects. + +| name | description | +| ----------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| [Provider](https://pub.dartlang.org/documentation/provider/latest/provider/Provider-class.html) | The most basic form of provider. It takes a value and exposes it, whatever the value is. | +| [ListenableProvider](https://pub.dartlang.org/documentation/provider/latest/provider/ListenableProvider-class.html) | A specific provider for Listenable object. ListenableProvider will listen to the object and ask widgets which depend on it to rebuild whenever the listener is called. | +| [ChangeNotifierProvider](https://pub.dartlang.org/documentation/provider/latest/provider/ChangeNotifierProvider-class.html) | A specification of ListenableProvider for ChangeNotifier. It will automatically call `ChangeNotifier.dispose` when needed. | +| [ValueListenableProvider](https://pub.dartlang.org/documentation/provider/latest/provider/ValueListenableProvider-class.html) | Listen to a ValueListenable and only expose `ValueListenable.value`. | +| [StreamProvider](https://pub.dartlang.org/documentation/provider/latest/provider/StreamProvider-class.html) | Listen to a Stream and expose the latest value emitted. | +| [FutureProvider](https://pub.dartlang.org/documentation/provider/latest/provider/FutureProvider-class.html) | Takes a `Future` and updates dependents when the future completes. | diff --git a/packages/provider/analysis_options.yaml b/packages/provider/analysis_options.yaml new file mode 100644 index 0000000..51d3cc0 --- /dev/null +++ b/packages/provider/analysis_options.yaml @@ -0,0 +1,82 @@ +include: package:pedantic/analysis_options.yaml +analyzer: + exclude: + - "**/*.g.dart" + strong-mode: + implicit-casts: false + implicit-dynamic: false + errors: + todo: error + include_file_not_found: ignore +linter: + rules: + - public_member_api_docs + - annotate_overrides + - avoid_empty_else + - avoid_function_literals_in_foreach_calls + - avoid_init_to_null + - avoid_null_checks_in_equality_operators + - avoid_relative_lib_imports + - avoid_renaming_method_parameters + - avoid_return_types_on_setters + - avoid_returning_null + - avoid_types_as_parameter_names + - avoid_unused_constructor_parameters + - await_only_futures + - camel_case_types + - cancel_subscriptions + - cascade_invocations + - comment_references + - constant_identifier_names + - control_flow_in_finally + - directives_ordering + - empty_catches + - empty_constructor_bodies + - empty_statements + - hash_and_equals + - implementation_imports + - invariant_booleans + - iterable_contains_unrelated_type + - library_names + - library_prefixes + - list_remove_unrelated_type + - no_adjacent_strings_in_list + - no_duplicate_case_values + - non_constant_identifier_names + - null_closures + - omit_local_variable_types + - only_throw_errors + - overridden_fields + - package_api_docs + - package_names + - package_prefixed_library_names + - prefer_adjacent_string_concatenation + - prefer_collection_literals + - prefer_conditional_assignment + - prefer_const_constructors + - prefer_contains + - prefer_equal_for_default_values + - prefer_final_fields + - prefer_initializing_formals + - prefer_interpolation_to_compose_strings + - prefer_is_empty + - prefer_is_not_empty + - prefer_single_quotes + - prefer_typing_uninitialized_variables + - recursive_getters + - slash_for_doc_comments + - test_types_in_equals + - throw_in_finally + - type_init_formals + - unawaited_futures + - unnecessary_brace_in_string_interps + - unnecessary_const + - unnecessary_getters_setters + - unnecessary_lambdas + - unnecessary_new + - unnecessary_null_aware_assignments + - unnecessary_statements + - unnecessary_this + - unrelated_type_equality_checks + - use_rethrow_when_possible + - valid_regexps diff --git a/packages/provider/packages/provider/.gitignore b/packages/provider/packages/provider/.gitignore new file mode 100644 index 0000000..7dfcccc --- /dev/null +++ b/packages/provider/packages/provider/.gitignore @@ -0,0 +1,14 @@ +# Files and directories created by pub +.dart_tool/ +android/ +ios/ +.packages +# Remove the following pattern if you wish to check in your lock file +pubspec.lock + +# Conventional directory for build outputs +build/ +coverage/ + +# Directory created by dartdoc +doc/api/ diff --git a/packages/provider/packages/provider/CHANGELOG.md b/packages/provider/packages/provider/CHANGELOG.md new file mode 100644 index 0000000..cb073ab --- /dev/null +++ b/packages/provider/packages/provider/CHANGELOG.md @@ -0,0 +1,79 @@ +# 3.0.0 + +## breaking (see the readme for migration steps): + +- `Provider` now throws if used with a `Listenable`/`Stream`. This can be disabled by setting + `Provider.debugCheckInvalidValueType` to `null`. +- The default constructor of `StreamProvider` has now builds a `Stream` + instead of `StreamController`. The previous behavior has been moved to `StreamProvider.controller`. +- All `XXProvider.value` constructors now use `value` as parameter name. +- Added `FutureProvider`, which takes a future and updates dependents when the future completes. +- Providers can no longer be instantiated using `const` constructors. + +## non-breaking: + +- Added `ProxyProvider`, `ListenableProxyProvider`, and `ChangeNotifierProxyProvider`. + These providers allows building values that depends on other providers, + without loosing reactivity or manually handling the state. +- Added `DelegateWidget` and a few related classes to help building custom providers. +- Exposed the internal generic `InheritedWidget` to help building custom providers. + +# 2.0.1 + +- fix a bug where `ListenableProvider.value`/`ChangeNotifierProvider.value` + /`StreamProvider.value`/`ValueListenableProvider.value` subscribed/unsubscribed + to their respective object too often +- fix a bug where `ListenableProvider.value`/`ChangeNotifierProvider.value` may + rebuild too often or skip some. + +# 2.0.0 + +- `Consumer` now takes an optional `child` argument for optimization purposes. +- merged `Provider` and `StatefulProvider` +- added a "builder" constructor to `ValueListenableProvider` +- normalized providers constructors such that the default constructor is a "builder", + and offer a `value` named constructor. + +# 1.6.1 + +- `Provider.of` now crashes with a `ProviderNotFoundException` when no `Provider` + are found in the ancestors of the context used. + +# 1.6.0 + +- new: `ChangeNotifierProvider`, similar to scoped_model that exposes `ChangeNotifer` subclass and + rebuilds dependents only when `notifyListeners` is called. +- new: `ValueListenableProvider`, a provider that rebuilds whenever the value passed + to a `ValueNotifier` change. + +# 1.5.0 + +- new: Add `Consumer` with up to 6 parameters. +- new: `MultiProvider`, a provider that makes a tree of provider more readable +- new: `StreamProvider`, a stream that exposes to its descendants the current value of a `Stream`. + +# 1.4.0 + +- Reintroduced `StatefulProvider` with a modified prototype. + The second argument of `valueBuilder` and `didChangeDependencies` have been removed. + And `valueBuilder` is now called only once for the whole life-cycle of `StatefulProvider`. + +# 1.3.0 + +- Added `Consumer`, useful when we need to both expose and consume a value simultaneously. + +# 1.2.0 + +- Added: `HookProvider`, a `Provider` that creates its value from a `Hook`. +- Deprecated `StatefulProvider`. Either make a `StatefulWidget` or use `HookProvider`. +- Integrated the widget inspector, so that `Provider` widget shows the current value. + +# 1.1.1 + +- add `didChangeDependencies` callback to allow updating the value based on an `InheritedWidget` +- add `updateShouldNotify` method to both `Provider` and `StatefulProvider` + +# 1.1.0 + +- `onDispose` has been added to `StatefulProvider` +- `BuildContext` is now passed to `valueBuilder` callback diff --git a/packages/provider/packages/provider/LICENSE b/packages/provider/packages/provider/LICENSE new file mode 100644 index 0000000..cdeef1d --- /dev/null +++ b/packages/provider/packages/provider/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2018 Remi Rousselet + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/packages/provider/packages/provider/README.md b/packages/provider/packages/provider/README.md new file mode 100644 index 0000000..f9c2963 --- /dev/null +++ b/packages/provider/packages/provider/README.md @@ -0,0 +1,182 @@ +[![Build Status](https://travis-ci.org/rrousselGit/provider.svg?branch=master)](https://travis-ci.org/rrousselGit/provider) +[![pub package](https://img.shields.io/pub/v/provider.svg)](https://pub.dartlang.org/packages/provider) [![codecov](https://codecov.io/gh/rrousselGit/provider/branch/master/graph/badge.svg)](https://codecov.io/gh/rrousselGit/provider) [![Gitter](https://badges.gitter.im/flutter_provider/community.svg)](https://gitter.im/flutter_provider/community?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) + +A dependency injection system built with widgets for widgets. `provider` is mostly syntax sugar for `InheritedWidget`, +to make common use-cases straightforward. + +## Migration from v2.0.0 to v3.0.0 + +- Providers can no longer be instantiated with `const`. +- `Provider` now throws if used with a `Listenable`/`Stream`. + Consider using `ListenableProvider`/`StreamProvider` instead. Alternatively, + this exception can be disabled by setting `Provider.debugCheckInvalidValueType` + to `null` like so: + +```dart +void main() { + Provider.debugCheckInvalidValueType = null; + + runApp(MyApp()); +} +``` + + +- All `XXProvider.value` constructors now use `value` as parameter name. + +Before: + +```dart +ChangeNotifierProvider.value(notifier: myNotifier), +``` + +After: + +```dart +ChangeNotifierProvider.value(value: myNotifier), +``` + +- `StreamProvider`'s default constructor now builds a `Stream` instead of `StreamController`. The previous behavior has been moved to the named constructor `StreamProvider.controller`. + +Before: + +```dart +StreamProvider(builder: (_) => StreamController()), +``` + +After: + +```dart +StreamProvider.controller(builder: (_) => StreamController()), +``` + +## Usage + +### Exposing a value + +To expose a variable using `provider`, wrap any widget into one of the provider widgets from this package +and pass it your variable. Then, all descendants of the newly added provider widget can access this variable. + +A simple example would be to wrap the entire application into a `Provider` widget and pass it our variable: + +```dart +Provider.value( + value: 'Hello World', + child: MaterialApp( + home: Home(), + ) +) +``` + +Alternatively, for complex objects, most providers expose a constructor that takes a function to create the value. +The provider will call that function only once, when inserting the widget in the tree, and expose the result. +This is perfect for exposing a complex object that never changes over time without writing a `StatefulWidget`. + +The following creates and exposes a `MyComplexClass`. And in the event where `Provider` is removed from the widget tree, +the instantiated `MyComplexClass` will be disposed. + +```dart +Provider( + builder: (context) => MyComplexClass(), + dispose: (context, value) => value.dispose() + child: SomeWidget(), +) +``` + +### Reading a value + +The easiest way to read a value is by using the static method `Provider.of(BuildContext context)`. This method will look +up in widget tree starting from the widget associated with the `BuildContext` passed and it will return the nearest variable +of type `T` found (or throw if nothing if found). + +Combined with the first example of [exposing a value](#exposing-a-value), this widget will read the exposed `String` and render "Hello World." + +```dart +class Home extends StatelessWidget { + @override + Widget build(BuildContext context) { + return Text( + /// Don't forget to pass the type of the object you want to obtain to `Provider.of`! + Provider.of(context) + ); + } +} +``` + +Alternatively instead of using `Provider.of`, we can use the `Consumer` widget. + +This can be useful for performance optimizations or when it is difficult to obtain a `BuildContext` descendant of the provider. + +```dart +Provider.value( + value: 'Hello World', + child: Consumer( + builder: (context, value, child) => Text(value), + ), +); +``` + +--- + +Note that you can freely use multiple providers with different types together: + +```dart +Provider.value( + value: 42, + child: Provider.value( + value: 'Hello World', + child: // ... + ) +) +``` + +And obtain their value independently: + +```dart +var value = Provider.of(context); +var value2 = Provider.of(context); +``` + +### MultiProvider + +When injecting many values in big applications, `Provider` can rapidly become pretty nested: + +```dart +Provider.value( + value: foo, + child: Provider.value( + value: bar, + child: Provider.value( + value: baz, + child: someWidget, + ) + ) +) +``` + +In that situation, we can use `MultiProvider` to improve the readability: + +```dart +MultiProvider( + providers: [ + Provider.value(value: foo), + Provider.value(value: bar), + Provider.value(value: baz), + ], + child: someWidget, +) +``` + +The behavior of both examples is strictly the same. `MultiProvider` only changes the appearance of the code. + +### Existing providers + +`provider` exposes a few different kinds of "provider" for different types of objects. + +| name | description | +| ----------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| [Provider](https://pub.dartlang.org/documentation/provider/latest/provider/Provider-class.html) | The most basic form of provider. It takes a value and exposes it, whatever the value is. | +| [ListenableProvider](https://pub.dartlang.org/documentation/provider/latest/provider/ListenableProvider-class.html) | A specific provider for Listenable object. ListenableProvider will listen to the object and ask widgets which depend on it to rebuild whenever the listener is called. | +| [ChangeNotifierProvider](https://pub.dartlang.org/documentation/provider/latest/provider/ChangeNotifierProvider-class.html) | A specification of ListenableProvider for ChangeNotifier. It will automatically call `ChangeNotifier.dispose` when needed. | +| [ValueListenableProvider](https://pub.dartlang.org/documentation/provider/latest/provider/ValueListenableProvider-class.html) | Listen to a ValueListenable and only expose `ValueListenable.value`. | +| [StreamProvider](https://pub.dartlang.org/documentation/provider/latest/provider/StreamProvider-class.html) | Listen to a Stream and expose the latest value emitted. | +| [FutureProvider](https://pub.dartlang.org/documentation/provider/latest/provider/FutureProvider-class.html) | Takes a `Future` and updates dependents when the future completes. | diff --git a/packages/provider/packages/provider/example/.gitignore b/packages/provider/packages/provider/example/.gitignore new file mode 100644 index 0000000..07488ba --- /dev/null +++ b/packages/provider/packages/provider/example/.gitignore @@ -0,0 +1,70 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# Visual Studio Code related +.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +.dart_tool/ +.flutter-plugins +.packages +.pub-cache/ +.pub/ +/build/ + +# Android related +**/android/**/gradle-wrapper.jar +**/android/.gradle +**/android/captures/ +**/android/gradlew +**/android/gradlew.bat +**/android/local.properties +**/android/**/GeneratedPluginRegistrant.java + +# iOS/XCode related +**/ios/**/*.mode1v3 +**/ios/**/*.mode2v3 +**/ios/**/*.moved-aside +**/ios/**/*.pbxuser +**/ios/**/*.perspectivev3 +**/ios/**/*sync/ +**/ios/**/.sconsign.dblite +**/ios/**/.tags* +**/ios/**/.vagrant/ +**/ios/**/DerivedData/ +**/ios/**/Icon? +**/ios/**/Pods/ +**/ios/**/.symlinks/ +**/ios/**/profile +**/ios/**/xcuserdata +**/ios/.generated/ +**/ios/Flutter/App.framework +**/ios/Flutter/Flutter.framework +**/ios/Flutter/Generated.xcconfig +**/ios/Flutter/app.flx +**/ios/Flutter/app.zip +**/ios/Flutter/flutter_assets/ +**/ios/ServiceDefinitions.json +**/ios/Runner/GeneratedPluginRegistrant.* + +# Exceptions to above rules. +!**/ios/**/default.mode1v3 +!**/ios/**/default.mode2v3 +!**/ios/**/default.pbxuser +!**/ios/**/default.perspectivev3 +!/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages diff --git a/packages/provider/packages/provider/example/.metadata b/packages/provider/packages/provider/example/.metadata new file mode 100644 index 0000000..170cb55 --- /dev/null +++ b/packages/provider/packages/provider/example/.metadata @@ -0,0 +1,10 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: b593f5167bce84fb3cad5c258477bf3abc1b14eb + channel: unknown + +project_type: app diff --git a/packages/provider/packages/provider/example/lib/main.dart b/packages/provider/packages/provider/example/lib/main.dart new file mode 100644 index 0000000..9e24fd4 --- /dev/null +++ b/packages/provider/packages/provider/example/lib/main.dart @@ -0,0 +1,125 @@ +// ignore_for_file: public_member_api_docs +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +void main() => runApp(MyApp()); + +class Counter with ChangeNotifier { + int _count = 0; + int get count => _count; + + void increment() { + _count++; + notifyListeners(); + } +} + +class MyApp extends StatelessWidget { + @override + Widget build(BuildContext context) { + return MultiProvider( + providers: [ + ChangeNotifierProvider(builder: (_) => Counter()), + ], + child: Consumer( + builder: (context, counter, _) { + return MaterialApp( + supportedLocales: const [Locale('en')], + localizationsDelegates: [ + DefaultMaterialLocalizations.delegate, + DefaultWidgetsLocalizations.delegate, + _ExampleLocalizationsDelegate(counter.count), + ], + home: const MyHomePage(), + ); + }, + ), + ); + } +} + +class ExampleLocalizations { + static ExampleLocalizations of(BuildContext context) => + Localizations.of(context, ExampleLocalizations); + + const ExampleLocalizations(this._count); + + final int _count; + + String get title => 'Tapped $_count times'; +} + +class _ExampleLocalizationsDelegate + extends LocalizationsDelegate { + const _ExampleLocalizationsDelegate(this.count); + + final int count; + + @override + bool isSupported(Locale locale) => locale.languageCode == 'en'; + + @override + Future load(Locale locale) => + SynchronousFuture(ExampleLocalizations(count)); + + @override + bool shouldReload(_ExampleLocalizationsDelegate old) => old.count != count; +} + +class MyHomePage extends StatelessWidget { + const MyHomePage({Key key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: const Title()), + body: const Center(child: CounterLabel()), + floatingActionButton: const IncrementCounterButton(), + ); + } +} + +class IncrementCounterButton extends StatelessWidget { + const IncrementCounterButton({Key key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return FloatingActionButton( + onPressed: Provider.of(context).increment, + tooltip: 'Increment', + child: const Icon(Icons.add), + ); + } +} + +class CounterLabel extends StatelessWidget { + const CounterLabel({Key key}) : super(key: key); + + @override + Widget build(BuildContext context) { + final counter = Provider.of(context); + return Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Text( + 'You have pushed the button this many times:', + ), + Text( + '${counter.count}', + style: Theme.of(context).textTheme.display1, + ), + ], + ); + } +} + +class Title extends StatelessWidget { + const Title({Key key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Text(ExampleLocalizations.of(context).title); + } +} diff --git a/packages/provider/packages/provider/example/pubspec.yaml b/packages/provider/packages/provider/example/pubspec.yaml new file mode 100644 index 0000000..9c6c693 --- /dev/null +++ b/packages/provider/packages/provider/example/pubspec.yaml @@ -0,0 +1,23 @@ +name: example +version: 1.0.0 +homepage: https://github.com/rrousselGit/provider +author: Remi Rousselet + +environment: + sdk: ">=2.1.0 <3.0.0" + +dependencies: + flutter: + sdk: flutter + provider: + +dev_dependencies: + flutter_test: + sdk: flutter + +dependency_overrides: + provider: + path: ../ + +flutter: + uses-material-design: true diff --git a/packages/provider/packages/provider/example/test/widget_test.dart b/packages/provider/packages/provider/example/test/widget_test.dart new file mode 100644 index 0000000..a446d40 --- /dev/null +++ b/packages/provider/packages/provider/example/test/widget_test.dart @@ -0,0 +1,35 @@ +// This is a basic Flutter widget test. +// +// To perform an interaction with a widget in your test, use the WidgetTester +// utility that Flutter provides. For example, you can send tap and scroll +// gestures. You can also use WidgetTester to find child widgets in the widget +// tree, read text, and verify that the values of widget properties are correct. + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'package:example/main.dart' as example; + +void main() { + testWidgets('Counter increments smoke test', (WidgetTester tester) async { + // Build our app and trigger a frame. + + example.main(); + + // Verify that our counter starts at 0. + expect(find.text('0'), findsOneWidget); + expect(find.text('1'), findsNothing); + expect(find.text('Tapped 0 times'), findsOneWidget); + expect(find.text('Tapped 1 times'), findsNothing); + + // Tap the '+' icon and trigger a frame. + await tester.tap(find.byIcon(Icons.add)); + await tester.pump(); + + // Verify that our counter has incremented. + expect(find.text('0'), findsNothing); + expect(find.text('1'), findsOneWidget); + expect(find.text('Tapped 0 times'), findsNothing); + expect(find.text('Tapped 1 times'), findsOneWidget); + }); +} diff --git a/packages/provider/packages/provider/lib/provider.dart b/packages/provider/packages/provider/lib/provider.dart new file mode 100644 index 0000000..f322893 --- /dev/null +++ b/packages/provider/packages/provider/lib/provider.dart @@ -0,0 +1,11 @@ +library provider; + +export 'src/async_provider.dart'; +export 'src/change_notifier_provider.dart'; +export 'src/consumer.dart'; +export 'src/delegate_widget.dart'; +export 'src/listenable_provider.dart'; +export 'src/provider.dart'; +export 'src/proxy_provider.dart' + hide NumericProxyProvider, Void, ProxyProviderBase; +export 'src/value_listenable_provider.dart'; diff --git a/packages/provider/packages/provider/lib/src/async_provider.dart b/packages/provider/packages/provider/lib/src/async_provider.dart new file mode 100644 index 0000000..ae4b78a --- /dev/null +++ b/packages/provider/packages/provider/lib/src/async_provider.dart @@ -0,0 +1,306 @@ +import 'dart:async'; + +import 'package:flutter_web/widgets.dart'; + +import 'delegate_widget.dart'; +import 'provider.dart'; + +/// A callback used to build a valid value from an error. +/// +/// See also: +/// +/// * [StreamProvider.catchError] which uses [ErrorBuilder] to handle errors +/// emitted by a [Stream]. +/// * [FutureProvider.catchError] which uses [ErrorBuilder] to handle +/// [Future.catch]. +typedef ErrorBuilder = T Function(BuildContext context, Object error); + +/// Listens to a [Stream] and exposes [T] to its descendants. +/// +/// It is considered an error to pass a stream that can emit errors without providing +/// a [catchError] method. +/// +/// {@template provider.streamprovider.initialdata} +/// [initialData] determines the value exposed until the [Stream] emits a value. +/// If omitted, defaults to `null`. +/// {@endtemplate} +/// +/// {@macro provider.updateshouldnotify} +/// +/// See also: +/// +/// * [Stream], which is listened by [StreamProvider]. +/// * [StreamController], to create a [Stream] +class StreamProvider extends ValueDelegateWidget> + implements SingleChildCloneableWidget { + /// Creates a [Stream] from [builder] and subscribes to it. + /// + /// The parameter [builder] must not be `null`. + StreamProvider({ + Key key, + @required ValueBuilder> builder, + T initialData, + ErrorBuilder catchError, + UpdateShouldNotify updateShouldNotify, + Widget child, + }) : this._( + key: key, + delegate: BuilderStateDelegate>(builder), + initialData: initialData, + catchError: catchError, + updateShouldNotify: updateShouldNotify, + child: child, + ); + + /// Creates a [StreamController] from [builder] and subscribes to its stream. + /// + /// [StreamProvider] will automatically call [StreamController.close] + /// when the widget is removed from the tree. + /// + /// The parameter [builder] must not be `null`. + StreamProvider.controller({ + Key key, + @required ValueBuilder> builder, + T initialData, + ErrorBuilder catchError, + UpdateShouldNotify updateShouldNotify, + Widget child, + }) : this._( + key: key, + delegate: _StreamControllerBuilderDelegate(builder), + initialData: initialData, + catchError: catchError, + updateShouldNotify: updateShouldNotify, + child: child, + ); + + /// Listens to [value] and expose it to all of [StreamProvider] descendants. + StreamProvider.value({ + Key key, + @required Stream value, + T initialData, + ErrorBuilder catchError, + UpdateShouldNotify updateShouldNotify, + Widget child, + }) : this._( + key: key, + delegate: SingleValueDelegate(value), + initialData: initialData, + catchError: catchError, + updateShouldNotify: updateShouldNotify, + child: child, + ); + + StreamProvider._({ + Key key, + @required ValueStateDelegate> delegate, + this.initialData, + this.catchError, + this.updateShouldNotify, + this.child, + }) : super(key: key, delegate: delegate); + + /// {@macro provider.streamprovider.initialdata} + final T initialData; + + /// The widget that is below the current [StreamProvider] widget in the tree. + /// + /// {@macro flutter.widgets.child} + final Widget child; + + /// An optional function used whenever the [Stream] emits an error. + /// + /// [catchError] will be called with the emitted error and + /// is expected to return a fallback value without throwing. + /// + /// The returned value will then be exposed to the descendants of [StreamProvider] + /// like any valid value. + final ErrorBuilder catchError; + + /// {@macro provider.updateshouldnotify} + final UpdateShouldNotify updateShouldNotify; + + @override + StreamProvider cloneWithChild(Widget child) { + return StreamProvider._( + key: key, + delegate: delegate, + updateShouldNotify: updateShouldNotify, + initialData: initialData, + catchError: catchError, + child: child, + ); + } + + @override + Widget build(BuildContext context) { + return StreamBuilder( + stream: delegate.value, + initialData: initialData, + builder: (_, snapshot) { + return InheritedProvider( + value: _snapshotToValue(snapshot, context, catchError, this), + child: child, + updateShouldNotify: updateShouldNotify, + ); + }, + ); + } +} + +T _snapshotToValue(AsyncSnapshot snapshot, BuildContext context, + ErrorBuilder catchError, ValueDelegateWidget owner) { + if (snapshot.hasError) { + if (catchError != null) { + return catchError(context, snapshot.error); + } + throw FlutterError(''' +An exception was throw by ${ + // ignore: invalid_use_of_protected_member + owner.delegate.value?.runtimeType} listened by +$owner, but no `catchError` was provided. + +Exception: +${snapshot.error} +'''); + } + return snapshot.data; +} + +class _StreamControllerBuilderDelegate + extends ValueStateDelegate> { + _StreamControllerBuilderDelegate(this._builder) : assert(_builder != null); + + StreamController _controller; + ValueBuilder> _builder; + + @override + Stream value; + + @override + void initDelegate() { + super.initDelegate(); + _controller = _builder(context); + value = _controller?.stream; + } + + @override + void didUpdateDelegate(_StreamControllerBuilderDelegate old) { + super.didUpdateDelegate(old); + value = old.value; + _controller = old._controller; + } + + @override + void dispose() { + _controller?.close(); + super.dispose(); + } +} + +/// Listens to a [Future] and exposes [T] to its descendants. +/// +/// It is considered an error to pass a future that can emit errors without providing +/// a [catchError] method. +/// +/// {@macro provider.updateshouldnotify} +/// +/// See also: +/// +/// * [Future], which is listened by [FutureProvider]. +class FutureProvider extends ValueDelegateWidget> + implements SingleChildCloneableWidget { + /// Creates a [Future] from [builder] and subscribes to it. + /// + /// [builder] must not be `null`. + FutureProvider({ + Key key, + @required ValueBuilder> builder, + T initialData, + ErrorBuilder catchError, + UpdateShouldNotify updateShouldNotify, + Widget child, + }) : this._( + key: key, + initialData: initialData, + catchError: catchError, + updateShouldNotify: updateShouldNotify, + delegate: BuilderStateDelegate(builder), + child: child, + ); + + /// Listens to [value] and expose it to all of [FutureProvider] descendants. + FutureProvider.value({ + Key key, + @required Future value, + T initialData, + ErrorBuilder catchError, + UpdateShouldNotify updateShouldNotify, + Widget child, + }) : this._( + key: key, + initialData: initialData, + catchError: catchError, + updateShouldNotify: updateShouldNotify, + delegate: SingleValueDelegate(value), + child: child, + ); + + FutureProvider._({ + Key key, + @required ValueStateDelegate> delegate, + this.initialData, + this.catchError, + this.updateShouldNotify, + this.child, + }) : super(key: key, delegate: delegate); + + /// [initialData] determines the value exposed until the [Future] completes. + /// + /// If omitted, defaults to `null`. + final T initialData; + + /// The widget that is below the current [FutureProvider] widget in the tree. + /// + /// {@macro flutter.widgets.child} + final Widget child; + + /// Optional function used if the [Future] emits an error. + /// + /// [catchError] will be called with the emitted error and + /// is expected to return a fallback value without throwing. + /// + /// The returned value will then be exposed to the descendants of [FutureProvider] + /// like any valid value. + final ErrorBuilder catchError; + + /// {@macro provider.updateshouldnotify} + final UpdateShouldNotify updateShouldNotify; + + @override + FutureProvider cloneWithChild(Widget child) { + return FutureProvider._( + key: key, + delegate: delegate, + updateShouldNotify: updateShouldNotify, + initialData: initialData, + catchError: catchError, + child: child, + ); + } + + @override + Widget build(BuildContext context) { + return FutureBuilder( + future: delegate.value, + initialData: initialData, + builder: (_, snapshot) { + return InheritedProvider( + value: _snapshotToValue(snapshot, context, catchError, this), + updateShouldNotify: updateShouldNotify, + child: child, + ); + }, + ); + } +} diff --git a/packages/provider/packages/provider/lib/src/change_notifier_provider.dart b/packages/provider/packages/provider/lib/src/change_notifier_provider.dart new file mode 100644 index 0000000..564be5d --- /dev/null +++ b/packages/provider/packages/provider/lib/src/change_notifier_provider.dart @@ -0,0 +1,223 @@ +import 'package:flutter_web/widgets.dart'; + +import 'delegate_widget.dart'; +import 'listenable_provider.dart'; +import 'provider.dart'; +import 'proxy_provider.dart'; + +/// Listens to a [ChangeNotifier], expose it to its descendants +/// and rebuilds dependents whenever the [ChangeNotifier.notifyListeners] is called. +/// +/// See also: +/// +/// * [ChangeNotifier], which is listened by [ChangeNotifierProvider]. +/// * [ListenableProvider], similar to [ChangeNotifierProvider] but works with any [Listenable]. +class ChangeNotifierProvider + extends ListenableProvider implements SingleChildCloneableWidget { + static void _disposer(BuildContext context, ChangeNotifier notifier) => + notifier?.dispose(); + + /// Create a [ChangeNotifier] using the [builder] function and automatically dispose it + /// when [ChangeNotifierProvider] is removed from the widget tree. + /// + /// [builder] must not be `null`. + ChangeNotifierProvider({ + Key key, + @required ValueBuilder builder, + Widget child, + }) : super(key: key, builder: builder, dispose: _disposer, child: child); + + /// Listens to [value] and expose it to all of [ChangeNotifierProvider] descendants. + ChangeNotifierProvider.value({ + Key key, + @required T value, + Widget child, + }) : super.value(key: key, value: value, child: child); +} + +class _NumericProxyProvider + extends ProxyProviderBase implements SingleChildCloneableWidget { + _NumericProxyProvider({ + Key key, + ValueBuilder initialBuilder, + @required this.builder, + this.child, + }) : assert(builder != null), + super( + key: key, + initialBuilder: initialBuilder, + dispose: ChangeNotifierProvider._disposer, + ); + + /// The widget that is below the current [Provider] widget in the + /// tree. + /// + /// {@macro flutter.widgets.child} + final Widget child; + + /// {@macro provider.proxyprovider.builder} + final Function builder; + + @override + _NumericProxyProvider cloneWithChild(Widget child) { + return _NumericProxyProvider( + key: key, + initialBuilder: initialBuilder, + builder: builder, + child: child, + ); + } + + @override + Widget build(BuildContext context, R value) { + return ChangeNotifierProvider.value( + value: value, + child: child, + ); + } + + @override + R didChangeDependencies(BuildContext context, R previous) { + final arguments = [ + context, + Provider.of(context), + ]; + + if (T2 != Void) arguments.add(Provider.of(context)); + if (T3 != Void) arguments.add(Provider.of(context)); + if (T4 != Void) arguments.add(Provider.of(context)); + if (T5 != Void) arguments.add(Provider.of(context)); + if (T6 != Void) arguments.add(Provider.of(context)); + + arguments.add(previous); + return Function.apply(builder, arguments) as R; + } +} + +/// {@macro provider.proxyprovider} +class ChangeNotifierProxyProvider + extends _NumericProxyProvider { + /// Initializes [key] for subclasses. + ChangeNotifierProxyProvider({ + Key key, + ValueBuilder initialBuilder, + @required ProxyProviderBuilder builder, + Widget child, + }) : super( + key: key, + initialBuilder: initialBuilder, + builder: builder, + child: child, + ); + + @override + ProxyProviderBuilder get builder => + super.builder as ProxyProviderBuilder; +} + +/// {@macro provider.proxyprovider} +class ChangeNotifierProxyProvider2 + extends _NumericProxyProvider { + /// Initializes [key] for subclasses. + ChangeNotifierProxyProvider2({ + Key key, + ValueBuilder initialBuilder, + @required ProxyProviderBuilder2 builder, + Widget child, + }) : super( + key: key, + initialBuilder: initialBuilder, + builder: builder, + child: child, + ); + + @override + ProxyProviderBuilder2 get builder => + super.builder as ProxyProviderBuilder2; +} + +/// {@macro provider.proxyprovider} +class ChangeNotifierProxyProvider3 + extends _NumericProxyProvider { + /// Initializes [key] for subclasses. + ChangeNotifierProxyProvider3({ + Key key, + ValueBuilder initialBuilder, + @required ProxyProviderBuilder3 builder, + Widget child, + }) : super( + key: key, + initialBuilder: initialBuilder, + builder: builder, + child: child, + ); + + @override + ProxyProviderBuilder3 get builder => + super.builder as ProxyProviderBuilder3; +} + +/// {@macro provider.proxyprovider} +class ChangeNotifierProxyProvider4 + extends _NumericProxyProvider { + /// Initializes [key] for subclasses. + ChangeNotifierProxyProvider4({ + Key key, + ValueBuilder initialBuilder, + @required ProxyProviderBuilder4 builder, + Widget child, + }) : super( + key: key, + initialBuilder: initialBuilder, + builder: builder, + child: child, + ); + + @override + ProxyProviderBuilder4 get builder => + super.builder as ProxyProviderBuilder4; +} + +/// {@macro provider.proxyprovider} + +class ChangeNotifierProxyProvider5 + extends _NumericProxyProvider { + /// Initializes [key] for subclasses. + ChangeNotifierProxyProvider5({ + Key key, + ValueBuilder initialBuilder, + @required ProxyProviderBuilder5 builder, + Widget child, + }) : super( + key: key, + initialBuilder: initialBuilder, + builder: builder, + child: child, + ); + + @override + ProxyProviderBuilder5 get builder => + super.builder as ProxyProviderBuilder5; +} + +/// {@macro provider.proxyprovider} +class ChangeNotifierProxyProvider6 + extends _NumericProxyProvider { + /// Initializes [key] for subclasses. + ChangeNotifierProxyProvider6({ + Key key, + ValueBuilder initialBuilder, + @required ProxyProviderBuilder6 builder, + Widget child, + }) : super( + key: key, + initialBuilder: initialBuilder, + builder: builder, + child: child, + ); + + @override + ProxyProviderBuilder6 get builder => + super.builder as ProxyProviderBuilder6; +} diff --git a/packages/provider/packages/provider/lib/src/consumer.dart b/packages/provider/packages/provider/lib/src/consumer.dart new file mode 100644 index 0000000..143f13c --- /dev/null +++ b/packages/provider/packages/provider/lib/src/consumer.dart @@ -0,0 +1,212 @@ +import 'package:flutter_web/widgets.dart'; + +import 'provider.dart'; + +/// {@template provider.consumer} +/// Obtain [Provider] from its ancestors and pass its value to [builder]. +/// +/// [builder] must not be null and may be called multiple times (such as when provided value change). +/// +/// ## Performance optimizations: +/// +/// {@macro provider.consumer.child} +/// {@endtemplate} +class Consumer extends StatelessWidget { + /// {@template provider.consumer.constructor} + /// Consumes a [Provider] + /// {@endtemplate} + Consumer({ + Key key, + @required this.builder, + this.child, + }) : assert(builder != null), + super(key: key); + + // fork of the documentation from https://docs.flutter.io/flutter/widgets/AnimatedBuilder/child.html + /// The child widget to pass to [builder]. + /// {@template provider.consumer.child} + /// + /// If a builder callback's return value contains a subtree that does not depend on the provided value, + /// it's more efficient to build that subtree once instead of rebuilding it on every change of the provided value. + /// + /// If the pre-built subtree is passed as the child parameter, [Consumer] will pass it back to the builder function so that it can be incorporated into the build. + /// + /// Using this pre-built child is entirely optional, but can improve performance significantly in some cases and is therefore a good practice. + /// {@endtemplate} + final Widget child; + + /// {@template provider.consumer.builder} + /// Build a widget tree based on the value from a [Provider]. + /// + /// Must not be null. + /// {@endtemplate} + final Widget Function(BuildContext context, T value, Widget child) builder; + + @override + Widget build(BuildContext context) { + return builder( + context, + Provider.of(context), + child, + ); + } +} + +/// {@macro provider.consumer} +class Consumer2 extends StatelessWidget { + /// {@macro provider.consumer.constructor} + Consumer2({ + Key key, + @required this.builder, + this.child, + }) : assert(builder != null), + super(key: key); + + /// The child widget to pass to [builder]. + /// + /// {@macro provider.consumer.child} + final Widget child; + + /// {@macro provider.consumer.builder} + final Widget Function(BuildContext context, A value, B value2, Widget chi) + builder; + + @override + Widget build(BuildContext context) { + return builder( + context, + Provider.of(context), + Provider.of(context), + child, + ); + } +} + +/// {@macro provider.consumer} +class Consumer3 extends StatelessWidget { + /// {@macro provider.consumer.constructor} + Consumer3({ + Key key, + @required this.builder, + this.child, + }) : assert(builder != null), + super(key: key); + + /// The child widget to pass to [builder]. + /// + /// {@macro provider.consumer.child} + final Widget child; + + /// {@macro provider.consumer.builder} + final Widget Function( + BuildContext context, A value, B value2, C value3, Widget child) builder; + + @override + Widget build(BuildContext context) { + return builder( + context, + Provider.of(context), + Provider.of(context), + Provider.of(context), + child, + ); + } +} + +/// {@macro provider.consumer} +class Consumer4 extends StatelessWidget { + /// {@macro provider.consumer.constructor} + Consumer4({ + Key key, + @required this.builder, + this.child, + }) : assert(builder != null), + super(key: key); + + /// The child widget to pass to [builder]. + /// + /// {@macro provider.consumer.child} + final Widget child; + + /// {@macro provider.consumer.builder} + final Widget Function(BuildContext context, A value, B value2, C value3, + D value4, Widget child) builder; + @override + Widget build(BuildContext context) { + return builder( + context, + Provider.of(context), + Provider.of(context), + Provider.of(context), + Provider.of(context), + child, + ); + } +} + +/// {@macro provider.consumer} +class Consumer5 extends StatelessWidget { + /// {@macro provider.consumer.constructor} + Consumer5({ + Key key, + @required this.builder, + this.child, + }) : assert(builder != null), + super(key: key); + + /// The child widget to pass to [builder]. + /// + /// {@macro provider.consumer.child} + final Widget child; + + /// {@macro provider.consumer.builder} + final Widget Function(BuildContext context, A value, B value2, C value3, + D value4, E value5, Widget child) builder; + + @override + Widget build(BuildContext context) { + return builder( + context, + Provider.of(context), + Provider.of(context), + Provider.of(context), + Provider.of(context), + Provider.of(context), + child, + ); + } +} + +/// {@macro provider.consumer} +class Consumer6 extends StatelessWidget { + /// {@macro provider.consumer.constructor} + Consumer6({ + Key key, + @required this.builder, + this.child, + }) : assert(builder != null), + super(key: key); + + /// The child widget to pass to [builder]. + /// + /// {@macro provider.consumer.child} + final Widget child; + + /// {@macro provider.consumer.builder} + final Widget Function(BuildContext context, A value, B value2, C value3, + D value4, E value5, F value6, Widget child) builder; + + @override + Widget build(BuildContext context) { + return builder( + context, + Provider.of(context), + Provider.of(context), + Provider.of(context), + Provider.of(context), + Provider.of(context), + Provider.of(context), + child, + ); + } +} diff --git a/packages/provider/packages/provider/lib/src/delegate_widget.dart b/packages/provider/packages/provider/lib/src/delegate_widget.dart new file mode 100644 index 0000000..6169ba2 --- /dev/null +++ b/packages/provider/packages/provider/lib/src/delegate_widget.dart @@ -0,0 +1,279 @@ +import 'package:flutter_web/widgets.dart'; +import 'package:provider/src/provider.dart' show Provider; + +/// A function that creates an object of type [T]. +/// +/// See also: +/// +/// * [BuilderStateDelegate] +typedef ValueBuilder = T Function(BuildContext context); + +/// A function that disposes an object of type [T]. +/// +/// See also: +/// +/// * [BuilderStateDelegate] +typedef Disposer = void Function(BuildContext context, T value); + +/// The state of a [DelegateWidget]. +/// +/// See also: +/// +/// * [ValueStateDelegate] +/// * [BuilderStateDelegate] +abstract class StateDelegate { + BuildContext _context; + + /// The location in the tree where this widget builds. + /// + /// See also [State.context]. + BuildContext get context => _context; + + StateSetter _setState; + + /// Notify the framework that the internal state of this object has changed. + /// + /// See the discussion on [State.setState] for more information. + @protected + StateSetter get setState => _setState; + + /// Called on [State.initState] or after [DelegateWidget] is rebuilt + /// with a [StateDelegate] of a different [runtimeType]. + @protected + @mustCallSuper + void initDelegate() {} + + /// Called whenever [State.didUpdateWidget] is called + /// + /// It is guaranteed for [old] to have the same [runtimeType] as `this`. + @protected + @mustCallSuper + void didUpdateDelegate(covariant StateDelegate old) {} + + /// Called when [DelegateWidget] is unmounted or if it is rebuilt + /// with a [StateDelegate] of a different [runtimeType]. + @protected + @mustCallSuper + void dispose() {} +} + +/// A [StatefulWidget] that delegates its [State] implementation to a [StateDelegate]. +/// +/// This is useful for widgets that must switch between different [State] implementation +/// under the same [runtimeType]. +/// +/// A typical use-case is a non-leaf widget with constructors that behaves differently, as it is necessary for +/// all of its constructors to share the same [runtimeType] or else its descendants would loose +/// their state. +/// +/// See also: +/// +/// * [StateDelegate], the equivalent of [State] but for [DelegateWidget]. +/// * [Provider], a concrete implementation of [DelegateWidget]. +abstract class DelegateWidget extends StatefulWidget { + /// Initializes [key] for subclasses. + /// + /// The argument [delegate] must not be `null`. + const DelegateWidget({ + Key key, + this.delegate, + }) : assert(delegate != null), + super(key: key); + + /// The current state of [DelegateWidget]. + /// + /// It should not be `null`. + @protected + final StateDelegate delegate; + + /// Describes the part of the user interface represented by this widget. + /// + /// It is fine for [build] to depend on the content of [delegate]. + /// + /// This method is strictly equivalent to [State.build]. + @protected + Widget build(BuildContext context); + + @override + StatefulElement createElement() => _DelegateElement(this); + + @override + _DelegateWidgetState createState() => _DelegateWidgetState(); +} + +class _DelegateWidgetState extends State { + @override + void initState() { + super.initState(); + _mountDelegate(); + _initDelegate(); + } + + void _initDelegate() { + assert(() { + (context as _DelegateElement)._debugIsInitDelegate = true; + return true; + }()); + widget.delegate.initDelegate(); + assert(() { + (context as _DelegateElement)._debugIsInitDelegate = false; + return true; + }()); + } + + void _mountDelegate() { + widget.delegate + .._context = context + .._setState = setState; + } + + void _unmountDelegate(StateDelegate delegate) { + delegate + .._context = null + .._setState = null; + } + + @override + void didUpdateWidget(DelegateWidget oldWidget) { + super.didUpdateWidget(oldWidget); + if (widget.delegate != oldWidget.delegate) { + _mountDelegate(); + if (widget.delegate.runtimeType != oldWidget.delegate.runtimeType) { + oldWidget.delegate.dispose(); + _initDelegate(); + } else { + widget.delegate.didUpdateDelegate(oldWidget.delegate); + } + _unmountDelegate(oldWidget.delegate); + } + } + + @override + Widget build(BuildContext context) => widget.build(context); + + @override + void dispose() { + widget.delegate.dispose(); + _unmountDelegate(widget.delegate); + super.dispose(); + } +} + +class _DelegateElement extends StatefulElement { + _DelegateElement(DelegateWidget widget) : super(widget); + + bool _debugIsInitDelegate = false; + + @override + DelegateWidget get widget => super.widget as DelegateWidget; + + @override + InheritedWidget inheritFromElement(Element ancestor, {Object aspect}) { + assert(() { + if (_debugIsInitDelegate) { + final targetType = ancestor.widget.runtimeType; + // error copied from StatefulElement + throw FlutterError( + 'inheritFromWidgetOfExactType($targetType) or inheritFromElement() was called before ${widget.delegate.runtimeType}.initDelegate() completed.\n' + 'When an inherited widget changes, for example if the value of Theme.of() changes, ' + 'its dependent widgets are rebuilt. If the dependent widget\'s reference to ' + 'the inherited widget is in a constructor or an initDelegate() method, ' + 'then the rebuilt dependent widget will not reflect the changes in the ' + 'inherited widget.\n' + 'Typically references to inherited widgets should occur in widget build() methods. Alternatively, ' + 'initialization based on inherited widgets can be placed in the didChangeDependencies method, which ' + 'is called after initDelegate and whenever the dependencies change thereafter.'); + } + return true; + }()); + return super.inheritFromElement(ancestor, aspect: aspect); + } +} + +/// A base class for [StateDelegate] that exposes a [value] of type [T]. +/// +/// See also: +/// +/// * [SingleValueDelegate], which extends [ValueStateDelegate] to store +/// an immutable value. +/// * [BuilderStateDelegate], which extends [ValueStateDelegate] +/// to build [value] from a function and dispose it when the widget is unmounted. +abstract class ValueStateDelegate extends StateDelegate { + /// The member [value] should not be mutated directly. + T get value; +} + +/// Stores an immutable value. +class SingleValueDelegate extends ValueStateDelegate { + /// Initializes [value] for subclasses. + SingleValueDelegate(this.value); + + @override + final T value; +} + +/// A [StateDelegate] that creates and dispose a value from functions. +/// +/// See also: +/// +/// * [ValueStateDelegate], which [BuilderStateDelegate] implements. +class BuilderStateDelegate extends ValueStateDelegate { + /// The parameter `builder` must not be `null`. + BuilderStateDelegate(this._builder, {Disposer dispose}) + : assert(_builder != null), + _dispose = dispose; + + /// A callback used to create [value]. + /// + /// Once [value] is initialized, [_builder] will never be called again + /// and [value] will never change. + /// + /// See also: + /// + /// * [value], which [_builder] creates. + final ValueBuilder _builder; + final Disposer _dispose; + + T _value; + @override + T get value => _value; + + @override + void initDelegate() { + super.initDelegate(); + _value = _builder(context); + } + + @override + void didUpdateDelegate(BuilderStateDelegate old) { + super.didUpdateDelegate(old); + _value = old.value; + } + + @override + void dispose() { + _dispose?.call(context, value); + super.dispose(); + } +} + +/// A [DelegateWidget] that accepts only [ValueStateDelegate] as [delegate]. +/// +/// See also: +/// +/// * [DelegateWidget] +/// * [ValueStateDelegate] +abstract class ValueDelegateWidget extends DelegateWidget { + /// Initializes [key] for subclasses. + /// + /// The argument [delegate] must not be `null`. + ValueDelegateWidget({ + Key key, + @required ValueStateDelegate delegate, + }) : super(key: key, delegate: delegate); + + @override + @protected + ValueStateDelegate get delegate => + super.delegate as ValueStateDelegate; +} diff --git a/packages/provider/packages/provider/lib/src/listenable_provider.dart b/packages/provider/packages/provider/lib/src/listenable_provider.dart new file mode 100644 index 0000000..e830351 --- /dev/null +++ b/packages/provider/packages/provider/lib/src/listenable_provider.dart @@ -0,0 +1,355 @@ +import 'package:flutter_web/foundation.dart'; +import 'package:flutter_web/widgets.dart'; + +import 'change_notifier_provider.dart' show ChangeNotifierProvider; +import 'delegate_widget.dart'; +import 'provider.dart'; +import 'proxy_provider.dart'; +import 'value_listenable_provider.dart' show ValueListenableProvider; + +/// Listens to a [Listenable], expose it to its descendants +/// and rebuilds dependents whenever the listener emits an event. +/// +/// See also: +/// +/// * [ChangeNotifierProvider], a subclass of [ListenableProvider] specific to [ChangeNotifier]. +/// * [ValueListenableProvider], which listens to a [ValueListenable] but exposes only [ValueListenable.value] instead of the whole object. +/// * [Listenable] +class ListenableProvider extends ValueDelegateWidget + implements SingleChildCloneableWidget { + /// Creates a [Listenable] using [builder] and subscribes to it. + /// + /// [dispose] can optionally passed to free resources + /// when [ListenableProvider] is removed from the tree. + /// + /// [builder] must not be `null`. + ListenableProvider({ + Key key, + @required ValueBuilder builder, + Disposer dispose, + Widget child, + }) : this._( + key: key, + delegate: _BuilderListenableDelegate(builder, dispose: dispose), + child: child, + ); + + /// Listens to [value] and expose it to all of [ListenableProvider] descendants. + /// + /// Rebuilding [ListenableProvider] without + /// changing the instance of [value] will not rebuild dependants. + ListenableProvider.value({ + Key key, + @required T value, + Widget child, + }) : this._( + key: key, + delegate: _ValueListenableDelegate(value), + child: child, + ); + + ListenableProvider._({ + Key key, + @required _ListenableDelegateMixin delegate, + // TODO: updateShouldNotify for when the listenable instance change with `.value` constructor + this.child, + }) : super( + key: key, + delegate: delegate, + ); + + /// The widget that is below the current [ListenableProvider] widget in the + /// tree. + /// + /// {@macro flutter.widgets.child} + final Widget child; + + @override + ListenableProvider cloneWithChild(Widget child) { + return ListenableProvider._( + key: key, + delegate: delegate as _ListenableDelegateMixin, + child: child, + ); + } + + @override + Widget build(BuildContext context) { + final delegate = this.delegate as _ListenableDelegateMixin; + return InheritedProvider( + value: delegate.value, + updateShouldNotify: delegate.updateShouldNotify, + child: child, + ); + } +} + +class _ValueListenableDelegate + extends SingleValueDelegate with _ListenableDelegateMixin { + _ValueListenableDelegate(T value) : super(value); + + @override + void didUpdateDelegate(_ValueListenableDelegate oldDelegate) { + super.didUpdateDelegate(oldDelegate); + if (oldDelegate.value != value) { + _removeListener?.call(); + if (value != null) startListening(value); + } + } +} + +class _BuilderListenableDelegate + extends BuilderStateDelegate with _ListenableDelegateMixin { + _BuilderListenableDelegate(ValueBuilder builder, {Disposer dispose}) + : super(builder, dispose: dispose); +} + +mixin _ListenableDelegateMixin + on ValueStateDelegate { + UpdateShouldNotify updateShouldNotify; + VoidCallback _removeListener; + + @override + void initDelegate() { + super.initDelegate(); + if (value != null) startListening(value); + } + + @override + void didUpdateDelegate(StateDelegate old) { + super.didUpdateDelegate(old); + final delegate = old as _ListenableDelegateMixin; + + _removeListener = delegate._removeListener; + updateShouldNotify = delegate.updateShouldNotify; + } + + void startListening(T listenable) { + /// The number of time [Listenable] called its listeners. + /// + /// It is used to differentiate external rebuilds from rebuilds caused by the listenable emitting an event. + /// This allows [InheritedWidget.updateShouldNotify] to return true only in the latter scenario. + var buildCount = 0; + final setState = this.setState; + final listener = () => setState(() => buildCount++); + + var capturedBuildCount = buildCount; + updateShouldNotify = (_, __) { + final res = buildCount != capturedBuildCount; + capturedBuildCount = buildCount; + return res; + }; + + listenable.addListener(listener); + _removeListener = () { + listenable.removeListener(listener); + _removeListener = null; + updateShouldNotify = null; + }; + } + + @override + void dispose() { + _removeListener?.call(); + super.dispose(); + } +} + +class _NumericProxyProvider + extends ProxyProviderBase implements SingleChildCloneableWidget { + _NumericProxyProvider({ + Key key, + ValueBuilder initialBuilder, + @required this.builder, + Disposer dispose, + this.child, + }) : assert(builder != null), + super( + key: key, + initialBuilder: initialBuilder, + dispose: dispose, + ); + + /// The widget that is below the current [Provider] widget in the + /// tree. + /// + /// {@macro flutter.widgets.child} + final Widget child; + + /// {@macro provider.proxyprovider.builder} + final Function builder; + + @override + _NumericProxyProvider cloneWithChild(Widget child) { + return _NumericProxyProvider( + key: key, + initialBuilder: initialBuilder, + builder: builder, + dispose: dispose, + child: child, + ); + } + + @override + Widget build(BuildContext context, R value) { + return ListenableProvider.value( + value: value, + child: child, + ); + } + + @override + R didChangeDependencies(BuildContext context, R previous) { + final arguments = [ + context, + Provider.of(context), + ]; + + if (T2 != Void) arguments.add(Provider.of(context)); + if (T3 != Void) arguments.add(Provider.of(context)); + if (T4 != Void) arguments.add(Provider.of(context)); + if (T5 != Void) arguments.add(Provider.of(context)); + if (T6 != Void) arguments.add(Provider.of(context)); + + arguments.add(previous); + return Function.apply(builder, arguments) as R; + } +} + +/// {@macro provider.proxyprovider} +class ListenableProxyProvider + extends _NumericProxyProvider { + /// Initializes [key] for subclasses. + ListenableProxyProvider({ + Key key, + ValueBuilder initialBuilder, + @required ProxyProviderBuilder builder, + Disposer dispose, + Widget child, + }) : super( + key: key, + initialBuilder: initialBuilder, + builder: builder, + dispose: dispose, + child: child, + ); + + @override + ProxyProviderBuilder get builder => + super.builder as ProxyProviderBuilder; +} + +/// {@macro provider.proxyprovider} +class ListenableProxyProvider2 + extends _NumericProxyProvider { + /// Initializes [key] for subclasses. + ListenableProxyProvider2({ + Key key, + ValueBuilder initialBuilder, + @required ProxyProviderBuilder2 builder, + Disposer dispose, + Widget child, + }) : super( + key: key, + initialBuilder: initialBuilder, + builder: builder, + dispose: dispose, + child: child, + ); + + @override + ProxyProviderBuilder2 get builder => + super.builder as ProxyProviderBuilder2; +} + +/// {@macro provider.proxyprovider} +class ListenableProxyProvider3 + extends _NumericProxyProvider { + /// Initializes [key] for subclasses. + ListenableProxyProvider3({ + Key key, + ValueBuilder initialBuilder, + @required ProxyProviderBuilder3 builder, + Disposer dispose, + Widget child, + }) : super( + key: key, + initialBuilder: initialBuilder, + builder: builder, + dispose: dispose, + child: child, + ); + + @override + ProxyProviderBuilder3 get builder => + super.builder as ProxyProviderBuilder3; +} + +/// {@macro provider.proxyprovider} +class ListenableProxyProvider4 + extends _NumericProxyProvider { + /// Initializes [key] for subclasses. + ListenableProxyProvider4({ + Key key, + ValueBuilder initialBuilder, + @required ProxyProviderBuilder4 builder, + Disposer dispose, + Widget child, + }) : super( + key: key, + initialBuilder: initialBuilder, + builder: builder, + dispose: dispose, + child: child, + ); + + @override + ProxyProviderBuilder4 get builder => + super.builder as ProxyProviderBuilder4; +} + +/// {@macro provider.proxyprovider} +class ListenableProxyProvider5 + extends _NumericProxyProvider { + /// Initializes [key] for subclasses. + ListenableProxyProvider5({ + Key key, + ValueBuilder initialBuilder, + @required ProxyProviderBuilder5 builder, + Disposer dispose, + Widget child, + }) : super( + key: key, + initialBuilder: initialBuilder, + builder: builder, + dispose: dispose, + child: child, + ); + + @override + ProxyProviderBuilder5 get builder => + super.builder as ProxyProviderBuilder5; +} + +/// {@macro provider.proxyprovider} +class ListenableProxyProvider6 + extends _NumericProxyProvider { + /// Initializes [key] for subclasses. + ListenableProxyProvider6({ + Key key, + ValueBuilder initialBuilder, + @required ProxyProviderBuilder6 builder, + Disposer dispose, + Widget child, + }) : super( + key: key, + initialBuilder: initialBuilder, + builder: builder, + dispose: dispose, + child: child, + ); + + @override + ProxyProviderBuilder6 get builder => + super.builder as ProxyProviderBuilder6; +} diff --git a/packages/provider/packages/provider/lib/src/provider.dart b/packages/provider/packages/provider/lib/src/provider.dart new file mode 100644 index 0000000..dcf4080 --- /dev/null +++ b/packages/provider/packages/provider/lib/src/provider.dart @@ -0,0 +1,347 @@ +import 'dart:async'; + +import 'package:flutter_web/foundation.dart'; +import 'package:flutter_web/widgets.dart'; +import 'package:provider/src/delegate_widget.dart'; + +/// A function that returns true when the update from [previous] to [current] +/// should notify listeners, if any. +/// +/// See also: +/// +/// * [InheritedWidget.updateShouldNotify] +typedef UpdateShouldNotify = bool Function(T previous, T current); + +/// Returns the type [T]. +/// See https://stackoverflow.com/questions/52891537/how-to-get-generic-type +/// and https://github.com/dart-lang/sdk/issues/11923. +Type _typeOf() => T; + +/// A base class for providers so that [MultiProvider] can regroup them into a +/// linear list. +abstract class SingleChildCloneableWidget implements Widget { + /// Clones the current provider with a new [child]. + /// + /// Note for implementers: all other values, including [Key] must be + /// preserved. + SingleChildCloneableWidget cloneWithChild(Widget child); +} + +/// A generic implementation of an [InheritedWidget]. +/// +/// Any descendant of this widget can obtain `value` using [Provider.of]. +/// +/// Do not use this class directly unless you are creating a custom "Provider". +/// Instead use [Provider] class, which wraps [InheritedProvider]. +class InheritedProvider extends InheritedWidget { + /// Allow customizing [updateShouldNotify]. + const InheritedProvider({ + Key key, + @required T value, + UpdateShouldNotify updateShouldNotify, + Widget child, + }) : _value = value, + _updateShouldNotify = updateShouldNotify, + super(key: key, child: child); + + /// The currently exposed value. + /// + /// Mutating `value` should be avoided. Instead rebuild the widget tree + /// and replace [InheritedProvider] with one that holds the new value. + final T _value; + final UpdateShouldNotify _updateShouldNotify; + + @override + bool updateShouldNotify(InheritedProvider oldWidget) { + if (_updateShouldNotify != null) { + return _updateShouldNotify(oldWidget._value, _value); + } + return oldWidget._value != _value; + } +} + +/// A provider that merges multiple providers into a single linear widget tree. +/// It is used to improve readability and reduce boilderplate code of having to +/// nest mutliple layers of providers. +/// +/// As such, we're going from: +/// +/// ```dart +/// Provider.value( +/// value: foo, +/// child: Provider.value( +/// value: bar, +/// child: Provider.value( +/// value: baz, +/// child: someWidget, +/// ) +/// ) +/// ) +/// ``` +/// +/// To: +/// +/// ```dart +/// MultiProvider( +/// providers: [ +/// Provider.value(value: foo), +/// Provider.value(value: bar), +/// Provider.value(value: baz), +/// ], +/// child: someWidget, +/// ) +/// ``` +/// +/// The widget tree representation of the two approaches are identical. +class MultiProvider extends StatelessWidget + implements SingleChildCloneableWidget { + /// Build a tree of providers from a list of [SingleChildCloneableWidget]. + const MultiProvider({ + Key key, + @required this.providers, + this.child, + }) : assert(providers != null), + super(key: key); + + /// The list of providers that will be transformed into a tree from top to + /// bottom. + /// + /// Example: with [A, B, C] and [child], the resulting widget tree looks like: + /// A + /// | + /// B + /// | + /// C + /// | + /// child + final List providers; + + /// The child of the last provider in [providers]. + /// + /// If [providers] is empty, [MultiProvider] just returns [child]. + final Widget child; + + @override + Widget build(BuildContext context) { + var tree = child; + for (final provider in providers.reversed) { + tree = provider.cloneWithChild(tree); + } + return tree; + } + + @override + MultiProvider cloneWithChild(Widget child) { + return MultiProvider( + key: key, + providers: providers, + child: child, + ); + } +} + +/// A [Provider] that manages the lifecycle of the value it provides by +/// delegating to a pair of [ValueBuilder] and [Disposer]. +/// +/// It is usually used to avoid making a [StatefulWidget] for something trivial, +/// such as instantiating a BLoC. +/// +/// [Provider] is the equivalent of a [State.initState] combined with +/// [State.dispose]. [ValueBuilder] is called only once in [State.initState]. +/// We cannot use [InheritedWidget] as it requires the value to be +/// constructor-initialized and final. +/// +/// The following example instantiates a `Model` once, and disposes it when +/// [Provider] is removed from the tree. +/// +/// {@template provider.updateshouldnotify} +/// [updateShouldNotify] can optionally be passed to avoid unnecessaryly rebuilding dependants when nothing changed. +/// Defaults to `(previous, next) => previous != next`. See [InheritedWidget.updateShouldNotify] for more informations. +/// {@endtemplate} +/// +/// ```dart +/// class Model { +/// void dispose() {} +/// } +/// +/// class Stateless extends StatelessWidget { +/// @override +/// Widget build(BuildContext context) { +/// return Provider( +/// builder: (context) => Model(), +/// dispose: (context, value) => value.dispose(), +/// child: ..., +/// ); +/// } +/// } +/// ``` +class Provider extends ValueDelegateWidget + implements SingleChildCloneableWidget { + /// Allows to specify parameters to [Provider]. + Provider({ + Key key, + @required ValueBuilder builder, + Disposer dispose, + Widget child, + }) : this._( + key: key, + delegate: BuilderStateDelegate(builder, dispose: dispose), + updateShouldNotify: null, + child: child, + ); + + /// Allows to specify parameters to [Provider]. + Provider.value({ + Key key, + @required T value, + UpdateShouldNotify updateShouldNotify, + Widget child, + }) : this._( + key: key, + delegate: SingleValueDelegate(value), + updateShouldNotify: updateShouldNotify, + child: child, + ); + + Provider._({ + Key key, + @required ValueStateDelegate delegate, + this.updateShouldNotify, + this.child, + }) : super(key: key, delegate: delegate); + + /// Obtains the nearest [Provider] up its widget tree and returns its value. + /// + /// If [listen] is `true` (default), later value changes will trigger a new + /// [State.build] to widgets, and [State.didChangeDependencies] for + /// [StatefulWidget]. + static T of(BuildContext context, {bool listen = true}) { + // this is required to get generic Type + final type = _typeOf>(); + final provider = listen + ? context.inheritFromWidgetOfExactType(type) as InheritedProvider + : context.ancestorInheritedElementForWidgetOfExactType(type)?.widget + as InheritedProvider; + + if (provider == null) { + throw ProviderNotFoundError(T, context.widget.runtimeType); + } + + return provider._value; + } + + /// A sanity check to prevent misuse of [Provider] when a variant should be used. + /// + /// By default, [debugCheckInvalidValueType] will throw if `value` is a [Listenable] + /// or a [Stream]. + /// In release mode, [debugCheckInvalidValueType] does nothing. + /// + /// This check can be disabled altogether by setting [debugCheckInvalidValueType] + /// to `null` like so: + /// + /// ```dart + /// void main() { + /// Provider.debugCheckInvalidValueType = null; + /// runApp(MyApp()); + /// } + /// ``` + static void Function(T value) debugCheckInvalidValueType = (T value) { + assert(() { + if (value is Listenable || value is Stream) { + throw FlutterError(''' +Tried to use Provider with a subtype of Listenable/Stream ($T). + +This is likely a mistake, as Provider will not automatically update dependents +when $T is updated. Instead, consider changing Provider for more specific +implementation that handles the update mecanism, such as: + +- ListenableProvider +- ChangeNotifierProvider +- ValueListenableProvider +- StreamProvider + +Alternatively, if you are making your own provider, consider using InheritedProvider. + +If you think that this is not an error, you can disable this check by setting +Provider.debugCheckInvalidValueType to `null` in your main file: + +``` +void main() { + Provider.debugCheckInvalidValueType = null; + + runApp(MyApp()); +} +``` +'''); + } + return true; + }()); + }; + + /// User-provided custom logic for [InheritedWidget.updateShouldNotify]. + final UpdateShouldNotify updateShouldNotify; + + @override + Provider cloneWithChild(Widget child) { + return Provider._( + key: key, + delegate: delegate, + updateShouldNotify: updateShouldNotify, + child: child, + ); + } + + /// The widget that is below the current [Provider] widget in the + /// tree. + /// + /// {@macro flutter.widgets.child} + final Widget child; + + @override + Widget build(BuildContext context) { + assert(() { + Provider.debugCheckInvalidValueType?.call(delegate.value); + return true; + }()); + return InheritedProvider( + value: delegate.value, + updateShouldNotify: updateShouldNotify, + child: child, + ); + } +} + +/// The error that will be thrown if [Provider.of] fails to find a +/// [Provider] as an ancestor of the [BuildContext] used. +class ProviderNotFoundError extends Error { + /// The type of the value being retrieved + final Type valueType; + + /// The type of the Widget requesting the value + final Type widgetType; + + /// Create a ProviderNotFound error with the type represented as a String. + ProviderNotFoundError( + this.valueType, + this.widgetType, + ); + + @override + String toString() { + return ''' +Error: Could not find the correct Provider<$valueType> above this $widgetType Widget + +To fix, please: + + * Ensure the Provider<$valueType> is an ancestor to this $widgetType Widget + * Provide types to Provider<$valueType> + * Provide types to Consumer<$valueType> + * Provide types to Provider.of<$valueType>() + * Always use package imports. Ex: `import 'package:my_app/my_code.dart'; + * Ensure the correct `context` is being used. + +If none of these solutions work, please file a bug at: +https://github.com/rrousselGit/provider/issues +'''; + } +} diff --git a/packages/provider/packages/provider/lib/src/proxy_provider.dart b/packages/provider/packages/provider/lib/src/proxy_provider.dart new file mode 100644 index 0000000..f4ceb80 --- /dev/null +++ b/packages/provider/packages/provider/lib/src/proxy_provider.dart @@ -0,0 +1,437 @@ +import 'package:flutter_web/widgets.dart'; +import 'package:provider/provider.dart'; + +import 'delegate_widget.dart'; +import 'provider.dart'; + +typedef ProviderBuilder = Widget Function( + BuildContext context, R value, Widget child); + +typedef ProxyProviderBuilder = R Function( + BuildContext context, T value, R previous); + +typedef ProxyProviderBuilder2 = R Function( + BuildContext context, T value, T2 value2, R previous); + +typedef ProxyProviderBuilder3 = R Function( + BuildContext context, T value, T2 value2, T3 value3, R previous); + +typedef ProxyProviderBuilder4 = R Function( + BuildContext context, T value, T2 value2, T3 value3, T4 value4, R previous); + +typedef ProxyProviderBuilder5 = R Function( + BuildContext context, + T value, + T2 value2, + T3 value3, + T4 value4, + T5 value5, + R previous, +); + +typedef ProxyProviderBuilder6 = R Function( + BuildContext context, + T value, + T2 value2, + T3 value3, + T4 value4, + T5 value5, + T6 value6, + R previous, +); + +/// A [StatefulWidget] that uses [ProxyProviderState] as [State]. +abstract class ProxyProviderWidget extends StatefulWidget { + /// Initializes [key] for subclasses. + const ProxyProviderWidget({Key key}) : super(key: key); + + @override + ProxyProviderState createState(); + + @override + ProxyProviderElement createElement() => ProxyProviderElement(this); +} + +/// A [State] with an added life-cycle: [didUpdateDependencies]. +/// +/// Widgets such as [ProxyProvider] are expected to build their +/// value from within [didUpdateDependencies] instead of [didChangeDependencies]. +abstract class ProxyProviderState + extends State { + /// To not confuse with [didChangeDependencies]. + /// + /// As opposed to [didChangeDependencies], [didUpdateDependencies] is + /// guaranteed to be followed by a call to [build], and will be called only + /// once after _all_ dependencies have changed. + /// + /// This guarantees that everything is up to date when [didUpdateDependencies] + /// is called, and that the widget will not be unmounted before updates are + /// applied. It is therefore safe to make network http calls or mutations + /// inside this life-cycle. + @protected + @mustCallSuper + void didUpdateDependencies() {} +} + +/// An [Element] that uses a [ProxyProviderWidget] as its configuration. +class ProxyProviderElement extends StatefulElement { + /// Creates an element that uses the given widget as its configuration. + ProxyProviderElement(ProxyProviderWidget widget) : super(widget); + + @override + ProxyProviderWidget get widget => super.widget as ProxyProviderWidget; + + @override + ProxyProviderState get state => + super.state as ProxyProviderState; + + bool _didChangeDependencies = true; + + @override + void didChangeDependencies() { + _didChangeDependencies = true; + super.didChangeDependencies(); + } + + @override + Widget build() { + if (_didChangeDependencies) { + _didChangeDependencies = false; + state.didUpdateDependencies(); + } + return super.build(); + } +} + +// ignore: public_member_api_docs +abstract class Void {} + +/// A base class for custom "Proxy provider". +/// +/// See [ProxyProvider] for a concrete implementation. +abstract class ProxyProviderBase extends ProxyProviderWidget { + /// Initializes [key], [initialBuilder] and [dispose] for subclasses. + ProxyProviderBase({ + Key key, + this.initialBuilder, + this.dispose, + }) : super(key: key); + + /// Builds the initial value passed as `previous` to [didChangeDependencies]. + /// + /// If omitted, [didChangeDependencies] will be called with `null` instead. + final ValueBuilder initialBuilder; + + /// Optionally allows to clean-up resources when the widget is removed from + /// the tree. + final Disposer dispose; + + @override + _ProxyProviderState createState() => _ProxyProviderState(); + + /// Builds the value passed to [build] by combining [InheritedWidget]. + /// + /// [didChangeDependencies] will be called once when the widget is mounted, + /// and once whenever any of the [InheritedWidget] which [ProxyProviderBase] + /// depends on updates. + /// + /// It is safe to perform side-effects in this method. + R didChangeDependencies(BuildContext context, R previous); + + /// An equivalent of [StatelessWidget.build]. + /// + /// `value` is the latest result of [didChangeDependencies]. + /// + /// [build] should avoid depending on [InheritedWidget]. Instead these + /// [InheritedWidget] should be used inside [didChangeDependencies]. + Widget build(BuildContext context, R value); +} + +class _ProxyProviderState extends ProxyProviderState> { + R _value; + + @override + void initState() { + super.initState(); + _value = widget.initialBuilder?.call(context); + } + + @override + void didUpdateDependencies() { + super.didUpdateDependencies(); + _value = widget.didChangeDependencies(context, _value); + } + + @override + Widget build(BuildContext context) => widget.build(context, _value); + + @override + void dispose() { + if (widget.dispose != null) { + widget.dispose(context, _value); + } + super.dispose(); + } +} + +@visibleForTesting +// ignore: public_member_api_docs +class NumericProxyProvider + extends ProxyProviderBase implements SingleChildCloneableWidget { + // ignore: public_member_api_docs + NumericProxyProvider({ + Key key, + ValueBuilder initialBuilder, + @required this.builder, + this.updateShouldNotify, + Disposer dispose, + this.child, + }) : assert(builder != null), + super( + key: key, + initialBuilder: initialBuilder, + dispose: dispose, + ); + + /// The widget that is below the current [Provider] widget in the + /// tree. + /// + /// {@macro flutter.widgets.child} + final Widget child; + + /// {@template provider.proxyprovider.builder} + /// Builds the value passed to [InheritedProvider] by combining [InheritedWidget]. + /// + /// [builder] will be called once when the widget is mounted, + /// and once whenever any of the [InheritedWidget] which [ProxyProvider] + /// depends on updates. + /// + /// It is safe to perform side-effects in this method. + /// {@endtemplate} + final Function builder; + + /// The [UpdateShouldNotify] passed to [InheritedProvider]. + final UpdateShouldNotify updateShouldNotify; + + @override + NumericProxyProvider cloneWithChild(Widget child) { + return NumericProxyProvider( + key: key, + initialBuilder: initialBuilder, + builder: builder, + updateShouldNotify: updateShouldNotify, + dispose: dispose, + child: child, + ); + } + + @override + Widget build(BuildContext context, R value) { + assert(() { + Provider.debugCheckInvalidValueType?.call(value); + return true; + }()); + return InheritedProvider( + value: value, + updateShouldNotify: updateShouldNotify, + child: child, + ); + } + + @override + R didChangeDependencies(BuildContext context, R previous) { + final arguments = [ + context, + Provider.of(context), + ]; + + if (T2 != Void) arguments.add(Provider.of(context)); + if (T3 != Void) arguments.add(Provider.of(context)); + if (T4 != Void) arguments.add(Provider.of(context)); + if (T5 != Void) arguments.add(Provider.of(context)); + if (T6 != Void) arguments.add(Provider.of(context)); + + arguments.add(previous); + return Function.apply(builder, arguments) as R; + } +} + +/// {@template provider.proxyprovider} +/// A provider that builds a value based on other providers. +/// +/// The exposed value is built through [builder], and then passed +/// to [InheritedProvider]. +/// +/// As opposed to the `builder` parameter of [Provider], [builder] +/// may be called more than once. It will be called once when the widget is +/// mounted, then once whenever any of the [InheritedWidget] which [ProxyProvider] +/// depends emits an update. +/// +/// [ProxyProvider] comes in different variants such as [ProxyProvider2]. +/// This only changes the [builder] function, such that it takes +/// a different number of arguments. +/// The `2` in [ProxyProvider2] means that [builder] builds its +/// value from **2** other providers. +/// +/// All variations of [builder] will receive the [BuildContext] +/// as first parameter, and the previously built value as last parameter. +/// +/// This previously built value will be `null` by default, unless +/// [initialBuilder] is specified – in which case, it will be the +/// value returned by [initialBuilder]. +/// +/// [builder] must not be `null`. +/// +/// See also: +/// +/// * [Provider], which matches the behavior of [ProxyProvider] without +/// dependending on other providers. +/// {@endtemplate} +class ProxyProvider + extends NumericProxyProvider { + /// Initializes [key] for subclasses. + ProxyProvider({ + Key key, + ValueBuilder initialBuilder, + @required ProxyProviderBuilder builder, + UpdateShouldNotify updateShouldNotify, + Disposer dispose, + Widget child, + }) : super( + key: key, + initialBuilder: initialBuilder, + builder: builder, + updateShouldNotify: updateShouldNotify, + dispose: dispose, + child: child, + ); + + @override + ProxyProviderBuilder get builder => + super.builder as ProxyProviderBuilder; +} + +/// {@macro provider.proxyprovider} +class ProxyProvider2 + extends NumericProxyProvider { + /// Initializes [key] for subclasses. + ProxyProvider2({ + Key key, + ValueBuilder initialBuilder, + @required ProxyProviderBuilder2 builder, + UpdateShouldNotify updateShouldNotify, + Disposer dispose, + Widget child, + }) : super( + key: key, + initialBuilder: initialBuilder, + builder: builder, + updateShouldNotify: updateShouldNotify, + dispose: dispose, + child: child, + ); + + @override + ProxyProviderBuilder2 get builder => + super.builder as ProxyProviderBuilder2; +} + +/// {@macro provider.proxyprovider} +class ProxyProvider3 + extends NumericProxyProvider { + /// Initializes [key] for subclasses. + ProxyProvider3({ + Key key, + ValueBuilder initialBuilder, + @required ProxyProviderBuilder3 builder, + UpdateShouldNotify updateShouldNotify, + Disposer dispose, + Widget child, + }) : super( + key: key, + initialBuilder: initialBuilder, + builder: builder, + updateShouldNotify: updateShouldNotify, + dispose: dispose, + child: child, + ); + + @override + ProxyProviderBuilder3 get builder => + super.builder as ProxyProviderBuilder3; +} + +/// {@macro provider.proxyprovider} +class ProxyProvider4 + extends NumericProxyProvider { + /// Initializes [key] for subclasses. + ProxyProvider4({ + Key key, + ValueBuilder initialBuilder, + @required ProxyProviderBuilder4 builder, + UpdateShouldNotify updateShouldNotify, + Disposer dispose, + Widget child, + }) : super( + key: key, + initialBuilder: initialBuilder, + builder: builder, + updateShouldNotify: updateShouldNotify, + dispose: dispose, + child: child, + ); + + @override + ProxyProviderBuilder4 get builder => + super.builder as ProxyProviderBuilder4; +} + +/// {@macro provider.proxyprovider} +class ProxyProvider5 + extends NumericProxyProvider { + /// Initializes [key] for subclasses. + ProxyProvider5({ + Key key, + ValueBuilder initialBuilder, + @required ProxyProviderBuilder5 builder, + UpdateShouldNotify updateShouldNotify, + Disposer dispose, + Widget child, + }) : super( + key: key, + initialBuilder: initialBuilder, + builder: builder, + updateShouldNotify: updateShouldNotify, + dispose: dispose, + child: child, + ); + + @override + ProxyProviderBuilder5 get builder => + super.builder as ProxyProviderBuilder5; +} + +/// {@macro provider.proxyprovider} +class ProxyProvider6 + extends NumericProxyProvider { + /// Initializes [key] for subclasses. + ProxyProvider6({ + Key key, + ValueBuilder initialBuilder, + @required ProxyProviderBuilder6 builder, + UpdateShouldNotify updateShouldNotify, + Disposer dispose, + Widget child, + }) : super( + key: key, + initialBuilder: initialBuilder, + builder: builder, + updateShouldNotify: updateShouldNotify, + dispose: dispose, + child: child, + ); + + @override + ProxyProviderBuilder6 get builder => + super.builder as ProxyProviderBuilder6; +} diff --git a/packages/provider/packages/provider/lib/src/value_listenable_provider.dart b/packages/provider/packages/provider/lib/src/value_listenable_provider.dart new file mode 100644 index 0000000..61ad905 --- /dev/null +++ b/packages/provider/packages/provider/lib/src/value_listenable_provider.dart @@ -0,0 +1,106 @@ +import 'package:flutter)_web/foundation.dart'; +import 'package:flutter)_web/widgets.dart'; + +import 'delegate_widget.dart'; +import 'listenable_provider.dart' show ListenableProvider; +import 'provider.dart'; + +/// Listens to a [ValueListenable] and expose its current value. +class ValueListenableProvider extends ValueDelegateWidget> + implements SingleChildCloneableWidget { + /// Creates a [ValueNotifier] using [builder] and automatically dispose it + /// when [ValueListenableProvider] is removed from the tree. + /// + /// [builder] must not be `null`. + /// + /// {@macro provider.updateshouldnotify} + /// + /// See also: + /// + /// * [ValueListenable] + /// * [ListenableProvider], similar to [ValueListenableProvider] but for any kind of [Listenable]. + ValueListenableProvider({ + Key key, + @required ValueBuilder> builder, + UpdateShouldNotify updateShouldNotify, + Widget child, + }) : this._( + key: key, + delegate: BuilderStateDelegate>( + builder, + dispose: _dispose, + ), + updateShouldNotify: updateShouldNotify, + child: child, + ); + + /// Listens to [value] and exposes its current value. + /// + /// Changing [value] will stop listening to the previous [value] and listen the new one. + /// Removing [ValueListenableProvider] from the tree will also stop listening to [value]. + /// + /// ```dart + /// ValueListenable foo; + /// + /// ValueListenableProvider.value( + /// valueListenable: foo, + /// child: Container(), + /// ); + /// ``` + ValueListenableProvider.value({ + Key key, + @required ValueListenable value, + UpdateShouldNotify updateShouldNotify, + Widget child, + }) : this._( + key: key, + delegate: SingleValueDelegate(value), + updateShouldNotify: updateShouldNotify, + child: child, + ); + + ValueListenableProvider._({ + Key key, + @required ValueStateDelegate> delegate, + this.updateShouldNotify, + this.child, + }) : super(key: key, delegate: delegate); + + static void _dispose(BuildContext context, ValueNotifier notifier) { + notifier.dispose(); + } + + /// The widget that is below the current [ValueListenableProvider] widget in the + /// tree. + /// + /// {@macro flutter.widgets.child} + final Widget child; + + /// {@macro provider.updateshouldnotify} + final UpdateShouldNotify updateShouldNotify; + + @override + ValueListenableProvider cloneWithChild(Widget child) { + return ValueListenableProvider._( + key: key, + delegate: delegate, + updateShouldNotify: updateShouldNotify, + child: child, + ); + } + + @override + Widget build(BuildContext context) { + return ValueListenableBuilder( + valueListenable: delegate.value, + builder: (_, value, child) { + return InheritedProvider( + value: value, + updateShouldNotify: updateShouldNotify, + child: child, + ); + }, + child: child, + ); + } +} diff --git a/packages/provider/packages/provider/pubspec.yaml b/packages/provider/packages/provider/pubspec.yaml new file mode 100644 index 0000000..7b1502c --- /dev/null +++ b/packages/provider/packages/provider/pubspec.yaml @@ -0,0 +1,30 @@ +name: provider +description: A dependency injection system built with widgets for widgets. provider is mostly syntax sugar for InheritedWidget, to make common use-cases straightforward. +version: 3.0.0+1 +homepage: https://github.com/rrousselGit/provider +authors: + - Remi Rousselet + - Flutter Team + +environment: + # You must be using Flutter >=1.5.0 or Dart >=2.3.0 + sdk: '>=2.3.0-dev.0.1 <3.0.0' + +dependencies: + flutter_web: any + flutter_web_ui: any + +dev_dependencies: + build_runner: ^1.4.0 + build_web_compilers: ^2.0.0 + pedantic: ^1.0.0 + +dependency_overrides: + flutter_web: + git: + url: https://github.com/flutter/flutter_web + path: packages/flutter_web + flutter_web_ui: + git: + url: https://github.com/flutter/flutter_web + path: packages/flutter_web_ui diff --git a/packages/provider/packages/provider/test/change_notifier_provider_test.dart b/packages/provider/packages/provider/test/change_notifier_provider_test.dart new file mode 100644 index 0000000..e886302 --- /dev/null +++ b/packages/provider/packages/provider/test/change_notifier_provider_test.dart @@ -0,0 +1,336 @@ +import 'package:flutter/widgets.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/mockito.dart'; +import 'package:provider/provider.dart'; +import 'package:flutter/foundation.dart'; + +import 'common.dart'; + +void main() { + group('ChangeNotifierProvider', () { + testWidgets('works with MultiProvider', (tester) async { + final key = GlobalKey(); + var notifier = ChangeNotifier(); + + await tester.pumpWidget(MultiProvider( + providers: [ + ChangeNotifierProvider.value(value: notifier), + ], + child: Container(key: key), + )); + + expect(Provider.of(key.currentContext), notifier); + }); + test('works with MultiProvider #2', () { + final provider = ChangeNotifierProvider.value( + key: const Key('42'), + value: ChangeNotifier(), + child: Container(), + ); + var child2 = Container(); + final clone = provider.cloneWithChild(child2); + + expect(clone.child, equals(child2)); + expect(clone.key, equals(provider.key)); + // ignore: invalid_use_of_protected_member + expect(clone.delegate, equals(provider.delegate)); + }); + test('works with MultiProvider #3', () { + final provider = ChangeNotifierProvider( + builder: (_) => ChangeNotifier(), + child: Container(), + key: const Key('42'), + ); + var child2 = Container(); + final clone = provider.cloneWithChild(child2); + + expect(clone.child, equals(child2)); + expect(clone.key, equals(provider.key)); + // ignore: invalid_use_of_protected_member + expect(clone.delegate, equals(provider.delegate)); + }); + group('default constructor', () { + testWidgets('pass down key', (tester) async { + final notifier = ChangeNotifier(); + final keyProvider = GlobalKey(); + + await tester.pumpWidget(ChangeNotifierProvider.value( + key: keyProvider, + value: notifier, + child: Container(), + )); + expect( + keyProvider.currentWidget, + isNotNull, + ); + }); + }); + testWidgets('works with null (default)', (tester) async { + final key = GlobalKey(); + await tester.pumpWidget(ChangeNotifierProvider.value( + value: null, + child: Container(key: key), + )); + + expect(Provider.of(key.currentContext), null); + }); + testWidgets('works with null (builder)', (tester) async { + final key = GlobalKey(); + await tester.pumpWidget(ChangeNotifierProvider( + builder: (_) => null, + child: Container(key: key), + )); + + expect(Provider.of(key.currentContext), null); + }); + group('stateful constructor', () { + testWidgets('called with context', (tester) async { + final builder = ValueBuilderMock(); + final key = GlobalKey(); + + await tester.pumpWidget(ChangeNotifierProvider( + key: key, + builder: builder, + child: Container(), + )); + verify(builder(key.currentContext)).called(1); + }); + test('throws if builder is null', () { + expect( + // ignore: prefer_const_constructors + () => ChangeNotifierProvider( + builder: null, + ), + throwsAssertionError, + ); + }); + testWidgets('pass down key', (tester) async { + final keyProvider = GlobalKey(); + + await tester.pumpWidget(ChangeNotifierProvider( + key: keyProvider, + builder: (_) => ChangeNotifier(), + child: Container(), + )); + expect( + keyProvider.currentWidget, + isNotNull, + ); + }); + }); + testWidgets('stateful builder called once', (tester) async { + final notifier = MockNotifier(); + final builder = ValueBuilderMock(); + when(builder(any)).thenReturn(notifier); + + await tester.pumpWidget(ChangeNotifierProvider( + builder: builder, + child: Container(), + )); + + final context = findElementOfWidget(); + + verify(builder(context)).called(1); + verifyNoMoreInteractions(builder); + clearInteractions(notifier); + + await tester.pumpWidget(ChangeNotifierProvider( + builder: builder, + child: Container(), + )); + + verifyNoMoreInteractions(builder); + verifyNoMoreInteractions(notifier); + }); + testWidgets('dispose called on unmount', (tester) async { + final notifier = MockNotifier(); + final builder = ValueBuilderMock(); + when(builder(any)).thenReturn(notifier); + + await tester.pumpWidget(ChangeNotifierProvider( + builder: builder, + child: Container(), + )); + + final context = findElementOfWidget(); + + verify(builder(context)).called(1); + verifyNoMoreInteractions(builder); + final listener = verify(notifier.addListener(captureAny)).captured.first + as VoidCallback; + clearInteractions(notifier); + + await tester.pumpWidget(Container()); + + verifyInOrder([notifier.removeListener(listener), notifier.dispose()]); + verifyNoMoreInteractions(builder); + verifyNoMoreInteractions(notifier); + }); + testWidgets('dispose can be null', (tester) async { + await tester.pumpWidget(ChangeNotifierProvider( + builder: (_) => ChangeNotifier(), + child: Container(), + )); + + await tester.pumpWidget(Container()); + }); + testWidgets( + 'Changing from default to stateful constructor calls stateful builder', + (tester) async { + final notifier = MockNotifier(); + var notifier2 = ChangeNotifier(); + final key = GlobalKey(); + await tester.pumpWidget(ChangeNotifierProvider.value( + value: notifier, + child: Container(), + )); + final listener = verify(notifier.addListener(captureAny)).captured.first + as VoidCallback; + clearInteractions(notifier); + + await tester.pumpWidget(ChangeNotifierProvider( + builder: (_) { + return notifier2; + }, + child: Container(key: key), + )); + + expect(Provider.of(key.currentContext), notifier2); + + await tester.pumpWidget(Container()); + verify(notifier.removeListener(listener)).called(1); + verifyNoMoreInteractions(notifier); + }); + testWidgets( + 'Changing from stateful to default constructor dispose correctly stateful notifier', + (tester) async { + final ChangeNotifier notifier = MockNotifier(); + var notifier2 = ChangeNotifier(); + final key = GlobalKey(); + + await tester.pumpWidget(ChangeNotifierProvider( + builder: (_) => notifier, + child: Container(), + )); + + final listener = verify(notifier.addListener(captureAny)).captured.first + as VoidCallback; + clearInteractions(notifier); + await tester.pumpWidget(ChangeNotifierProvider.value( + value: notifier2, + child: Container(key: key), + )); + + expect(Provider.of(key.currentContext), notifier2); + + await tester.pumpWidget(Container()); + + verifyInOrder([ + notifier.removeListener(listener), + notifier.dispose(), + ]); + verifyNoMoreInteractions(notifier); + }); + testWidgets('dispose can be null', (tester) async { + await tester.pumpWidget(ChangeNotifierProvider( + builder: (_) => ChangeNotifier(), + child: Container(), + )); + + await tester.pumpWidget(Container()); + }); + testWidgets('changing notifier rebuilds descendants', (tester) async { + final builder = BuilderMock(); + when(builder(any)).thenReturn(Container()); + + var notifier = ChangeNotifier(); + Widget build() { + return ChangeNotifierProvider.value( + value: notifier, + child: Builder(builder: (context) { + Provider.of(context); + return builder(context); + }), + ); + } + + await tester.pumpWidget(build()); + + verify(builder(any)).called(1); + + // ignore: invalid_use_of_protected_member + expect(notifier.hasListeners, true); + + var previousNotifier = notifier; + notifier = ChangeNotifier(); + await tester.pumpWidget(build()); + + // ignore: invalid_use_of_protected_member + expect(notifier.hasListeners, true); + // ignore: invalid_use_of_protected_member + expect(previousNotifier.hasListeners, false); + + verify(builder(any)).called(1); + + await tester.pumpWidget(Container()); + + // ignore: invalid_use_of_protected_member + expect(notifier.hasListeners, false); + }); + testWidgets("rebuilding with the same provider don't rebuilds descendants", + (tester) async { + final notifier = ChangeNotifier(); + final keyChild = GlobalKey(); + final builder = BuilderMock(); + when(builder(any)).thenReturn(Container()); + + final child = Builder( + key: keyChild, + builder: builder, + ); + + await tester.pumpWidget(ChangeNotifierProvider.value( + value: notifier, + child: child, + )); + + verify(builder(any)).called(1); + expect(Provider.of(keyChild.currentContext), notifier); + + await tester.pumpWidget(ChangeNotifierProvider.value( + value: notifier, + child: child, + )); + verifyNoMoreInteractions(builder); + expect(Provider.of(keyChild.currentContext), notifier); + }); + testWidgets('notifylistener rebuilds descendants', (tester) async { + final notifier = ChangeNotifier(); + final keyChild = GlobalKey(); + final builder = BuilderMock(); + when(builder(any)).thenReturn(Container()); + + final child = Builder( + key: keyChild, + builder: (context) { + // subscribe + Provider.of(context); + return builder(context); + }, + ); + var changeNotifierProvider = ChangeNotifierProvider.value( + value: notifier, + child: child, + ); + await tester.pumpWidget(changeNotifierProvider); + + clearInteractions(builder); + // ignore: invalid_use_of_protected_member + notifier.notifyListeners(); + await Future.value(); + await tester.pump(); + verify(builder(any)).called(1); + expect(Provider.of(keyChild.currentContext), notifier); + }); + }); +} diff --git a/packages/provider/packages/provider/test/change_notifier_proxy_provider_test.dart b/packages/provider/packages/provider/test/change_notifier_proxy_provider_test.dart new file mode 100644 index 0000000..e3d5dd1 --- /dev/null +++ b/packages/provider/packages/provider/test/change_notifier_proxy_provider_test.dart @@ -0,0 +1,258 @@ +import 'package:flutter/widgets.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/mockito.dart'; +import 'package:provider/provider.dart'; +import 'package:provider/src/proxy_provider.dart' show ProxyProviderBase; + +import 'common.dart'; + +class _ListenableCombined = Combined with ChangeNotifier; + +void main() { + final a = A(); + final b = B(); + final c = C(); + final d = D(); + final e = E(); + final f = F(); + + final combinedConsumerMock = ConsumerBuilderMock(); + setUp(() => when(combinedConsumerMock(any)).thenReturn(Container())); + tearDown(() { + clearInteractions(combinedConsumerMock); + }); + + final mockConsumer = Consumer<_ListenableCombined>( + builder: (context, combined, child) => combinedConsumerMock(combined), + ); + + group('ChangeNotifierProxyProvider', () { + test('throws if builder is missing', () { + expect( + () => + ChangeNotifierProxyProvider(builder: null), + throwsAssertionError, + ); + }); + + testWidgets('works with null', (tester) async { + await tester.pumpWidget( + MultiProvider( + providers: [ + Provider.value(value: 0), + ChangeNotifierProxyProvider( + initialBuilder: (_) => null, + builder: (_, __, value) => value, + ) + ], + child: Container(), + ), + ); + + await tester.pumpWidget(Container()); + }); + + testWidgets('rebuilds dependendents when listeners are called', + (tester) async { + final notifier = ValueNotifier(0); + await tester.pumpWidget( + MultiProvider( + providers: [ + Provider.value(value: 0), + ChangeNotifierProxyProvider>( + initialBuilder: (_) => notifier, + builder: (_, count, value) => value..value = count, + ) + ], + child: Consumer>(builder: (_, value, __) { + return Text( + value.value.toString(), + textDirection: TextDirection.ltr, + ); + }), + ), + ); + + expect(find.text('0'), findsOneWidget); + expect(find.text('1'), findsNothing); + + notifier.value++; + await tester.pump(); + + expect(find.text('1'), findsOneWidget); + expect(find.text('0'), findsNothing); + }); + testWidgets('disposes of created value', (tester) async { + final notifier = MockNotifier(); + final key = GlobalKey(); + + await tester.pumpWidget( + MultiProvider( + providers: [ + Provider.value(value: 0), + ChangeNotifierProxyProvider( + key: key, + initialBuilder: (_) => notifier, + builder: (_, count, value) => value, + ) + ], + child: Container(), + ), + ); + + await tester.pumpWidget(Container()); + + verify(notifier.dispose()).called(1); + }); + }); + + group('ChangeNotifierProxyProvider variants', () { + Finder findProxyProvider() => find + .byWidgetPredicate((widget) => widget is ProxyProviderBase); + testWidgets('ChangeNotifierProxyProvider2', (tester) async { + await tester.pumpWidget( + MultiProvider( + providers: [ + Provider.value(value: a), + Provider.value(value: b), + Provider.value(value: c), + Provider.value(value: d), + Provider.value(value: e), + Provider.value(value: f), + ChangeNotifierProxyProvider2( + initialBuilder: (_) => _ListenableCombined(null, null, null), + builder: (context, a, b, previous) => + _ListenableCombined(context, previous, a, b), + ) + ], + child: mockConsumer, + ), + ); + + final context = tester.element(findProxyProvider()); + + verify( + combinedConsumerMock( + _ListenableCombined( + context, _ListenableCombined(null, null, null), a, b), + ), + ).called(1); + }); + testWidgets('ChangeNotifierProxyProvider3', (tester) async { + await tester.pumpWidget( + MultiProvider( + providers: [ + Provider.value(value: a), + Provider.value(value: b), + Provider.value(value: c), + Provider.value(value: d), + Provider.value(value: e), + Provider.value(value: f), + ChangeNotifierProxyProvider3( + initialBuilder: (_) => _ListenableCombined(null, null, null), + builder: (context, a, b, c, previous) => + _ListenableCombined(context, previous, a, b, c), + ) + ], + child: mockConsumer, + ), + ); + + final context = tester.element(findProxyProvider()); + + verify( + combinedConsumerMock( + _ListenableCombined( + context, _ListenableCombined(null, null, null), a, b, c), + ), + ).called(1); + }); + testWidgets('ChangeNotifierProxyProvider4', (tester) async { + await tester.pumpWidget( + MultiProvider( + providers: [ + Provider.value(value: a), + Provider.value(value: b), + Provider.value(value: c), + Provider.value(value: d), + Provider.value(value: e), + Provider.value(value: f), + ChangeNotifierProxyProvider4( + initialBuilder: (_) => _ListenableCombined(null, null, null), + builder: (context, a, b, c, d, previous) => + _ListenableCombined(context, previous, a, b, c, d), + ) + ], + child: mockConsumer, + ), + ); + + final context = tester.element(findProxyProvider()); + + verify( + combinedConsumerMock( + _ListenableCombined( + context, _ListenableCombined(null, null, null), a, b, c, d), + ), + ).called(1); + }); + testWidgets('ChangeNotifierProxyProvider5', (tester) async { + await tester.pumpWidget( + MultiProvider( + providers: [ + Provider.value(value: a), + Provider.value(value: b), + Provider.value(value: c), + Provider.value(value: d), + Provider.value(value: e), + Provider.value(value: f), + ChangeNotifierProxyProvider5( + initialBuilder: (_) => _ListenableCombined(null, null, null), + builder: (context, a, b, c, d, e, previous) => + _ListenableCombined(context, previous, a, b, c, d, e), + ) + ], + child: mockConsumer, + ), + ); + + final context = tester.element(findProxyProvider()); + + verify( + combinedConsumerMock( + _ListenableCombined(context, _ListenableCombined(null, null, null), a, + b, c, d, e, null), + ), + ).called(1); + }); + testWidgets('ChangeNotifierProxyProvider6', (tester) async { + await tester.pumpWidget( + MultiProvider( + providers: [ + Provider.value(value: a), + Provider.value(value: b), + Provider.value(value: c), + Provider.value(value: d), + Provider.value(value: e), + Provider.value(value: f), + ChangeNotifierProxyProvider6( + initialBuilder: (_) => _ListenableCombined(null, null, null), + builder: (context, a, b, c, d, e, f, previous) => + _ListenableCombined(context, previous, a, b, c, d, e, f), + ) + ], + child: mockConsumer, + ), + ); + + final context = tester.element(findProxyProvider()); + + verify( + combinedConsumerMock( + _ListenableCombined( + context, _ListenableCombined(null, null, null), a, b, c, d, e, f), + ), + ).called(1); + }); + }); +} diff --git a/packages/provider/packages/provider/test/common.dart b/packages/provider/packages/provider/test/common.dart new file mode 100644 index 0000000..8d8553b --- /dev/null +++ b/packages/provider/packages/provider/test/common.dart @@ -0,0 +1,106 @@ +import 'dart:async'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/mockito.dart'; + +Element findElementOfWidget() { + return find.byType(T).first.evaluate().first; +} + +Type typeOf() => T; + +class ValueBuilderMock extends Mock { + T call(BuildContext context); +} + +class DisposerMock extends Mock { + void call(BuildContext context, T value); +} + +class MockNotifier extends Mock implements ChangeNotifier {} + +class BuilderMock extends Mock { + Widget call(BuildContext context); +} + +class UpdateShouldNotifyMock extends Mock { + bool call(T old, T newValue); +} + +class A with DiagnosticableTreeMixin {} + +class B with DiagnosticableTreeMixin {} + +class C with DiagnosticableTreeMixin {} + +class D with DiagnosticableTreeMixin {} + +class E with DiagnosticableTreeMixin {} + +class F with DiagnosticableTreeMixin {} + +class ConsumerBuilderMock extends Mock { + Widget call(Combined foo); +} + +class CombinerMock extends Mock { + Combined call(BuildContext context, A a, Combined foo); +} + +class ProviderBuilderMock extends Mock { + Widget call(BuildContext context, Combined value, Widget child); +} + +class Combined extends DiagnosticableTree { + final A a; + final B b; + final C c; + final D d; + final E e; + final F f; + final Combined previous; + final BuildContext context; + + Combined(this.context, this.previous, this.a, + [this.b, this.c, this.d, this.e, this.f]); + + @override + // ignore: hash_and_equals + bool operator ==(Object other) => + other is Combined && + other.context == context && + other.previous == previous && + other.a == a && + other.b == b && + other.c == c && + other.e == e && + other.f == f; + + // fancy toString for debug purposes. + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.properties.addAll([ + DiagnosticsProperty('a', a, defaultValue: null), + DiagnosticsProperty('b', b, defaultValue: null), + DiagnosticsProperty('c', c, defaultValue: null), + DiagnosticsProperty('d', d, defaultValue: null), + DiagnosticsProperty('e', e, defaultValue: null), + DiagnosticsProperty('f', f, defaultValue: null), + DiagnosticsProperty('previous', previous, defaultValue: null), + DiagnosticsProperty('context', context, defaultValue: null), + ]); + } +} + +class MyListenable extends ChangeNotifier {} + +class MyStream extends Stream { + @override + StreamSubscription listen(void Function(void event) onData, + {Function onError, void Function() onDone, bool cancelOnError}) { + return null; + } +} diff --git a/packages/provider/packages/provider/test/consumer_test.dart b/packages/provider/packages/provider/test/consumer_test.dart new file mode 100644 index 0000000..41b14fe --- /dev/null +++ b/packages/provider/packages/provider/test/consumer_test.dart @@ -0,0 +1,215 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/mockito.dart'; +import 'package:provider/provider.dart'; + +import 'common.dart'; + +class ConsumerBuilderMock extends Mock { + Widget call(Combined foo); +} + +class Combined { + final A a; + final B b; + final C c; + final D d; + final E e; + final F f; + final Widget child; + final BuildContext context; + + Combined(this.context, this.child, this.a, + [this.b, this.c, this.d, this.e, this.f]); + + @override + // ignore: hash_and_equals + bool operator ==(Object other) => + other is Combined && + other.context == context && + other.child == child && + other.a == a && + other.b == b && + other.c == c && + other.e == e && + other.f == f; +} + +void main() { + final a = A(); + final b = B(); + final c = C(); + final d = D(); + final e = E(); + final f = F(); + final provider = MultiProvider( + providers: [ + Provider.value(value: a), + Provider.value(value: b), + Provider.value(value: c), + Provider.value(value: d), + Provider.value(value: e), + Provider.value(value: f), + ], + ); + + final mock = ConsumerBuilderMock(); + setUp(() { + when(mock(any)).thenReturn(Container()); + }); + tearDown(() { + clearInteractions(mock); + }); + + group('consumer', () { + testWidgets('obtains value from Provider', (tester) async { + final key = GlobalKey(); + final child = Container(); + + await tester.pumpWidget( + provider.cloneWithChild( + Consumer( + key: key, + builder: (context, value, child) => + mock(Combined(context, child, value)), + child: child, + ), + ), + ); + + verify(mock(Combined(key.currentContext, child, a))); + }); + testWidgets('crashed with no builder', (tester) async { + expect( + () => Consumer(builder: null), + throwsAssertionError, + ); + }); + }); + + group('consumer2', () { + testWidgets('obtains value from Provider', (tester) async { + final key = GlobalKey(); + final child = Container(); + + await tester.pumpWidget( + provider.cloneWithChild( + Consumer2( + key: key, + builder: (context, value, v2, child) => + mock(Combined(context, child, value, v2)), + child: child, + ), + ), + ); + + verify(mock(Combined(key.currentContext, child, a, b))); + }); + testWidgets('crashed with no builder', (tester) async { + expect( + () => Consumer2(builder: null), + throwsAssertionError, + ); + }); + }); + group('consumer3', () { + testWidgets('obtains value from Provider', (tester) async { + final key = GlobalKey(); + final child = Container(); + + await tester.pumpWidget( + provider.cloneWithChild( + Consumer3( + key: key, + builder: (context, value, v2, v3, child) => + mock(Combined(context, child, value, v2, v3)), + child: child, + ), + ), + ); + + verify(mock(Combined(key.currentContext, child, a, b, c))); + }); + testWidgets('crashed with no builder', (tester) async { + expect( + () => Consumer3(builder: null), + throwsAssertionError, + ); + }); + }); + group('consumer4', () { + testWidgets('obtains value from Provider', (tester) async { + final key = GlobalKey(); + final child = Container(); + + await tester.pumpWidget( + provider.cloneWithChild( + Consumer4( + key: key, + builder: (context, value, v2, v3, v4, child) => + mock(Combined(context, child, value, v2, v3, v4)), + child: child, + ), + ), + ); + + verify(mock(Combined(key.currentContext, child, a, b, c, d))); + }); + testWidgets('crashed with no builder', (tester) async { + expect( + () => Consumer4(builder: null), + throwsAssertionError, + ); + }); + }); + group('consumer5', () { + testWidgets('obtains value from Provider', (tester) async { + final key = GlobalKey(); + final child = Container(); + + await tester.pumpWidget( + provider.cloneWithChild( + Consumer5( + key: key, + builder: (context, value, v2, v3, v4, v5, child) => + mock(Combined(context, child, value, v2, v3, v4, v5)), + child: child, + ), + ), + ); + + verify(mock(Combined(key.currentContext, child, a, b, c, d, e))); + }); + testWidgets('crashed with no builder', (tester) async { + expect( + () => Consumer5(builder: null), + throwsAssertionError, + ); + }); + }); + group('consumer6', () { + testWidgets('obtains value from Provider', (tester) async { + final key = GlobalKey(); + final child = Container(); + + await tester.pumpWidget( + provider.cloneWithChild( + Consumer6( + key: key, + builder: (context, value, v2, v3, v4, v5, v6, child) => + mock(Combined(context, child, value, v2, v3, v4, v5, v6)), + child: child, + ), + ), + ); + + verify(mock(Combined(key.currentContext, child, a, b, c, d, e, f))); + }); + testWidgets('crashed with no builder', (tester) async { + expect( + () => Consumer6(builder: null), + throwsAssertionError, + ); + }); + }); +} diff --git a/packages/provider/packages/provider/test/delegate_widget_test.dart b/packages/provider/packages/provider/test/delegate_widget_test.dart new file mode 100644 index 0000000..ca56a13 --- /dev/null +++ b/packages/provider/packages/provider/test/delegate_widget_test.dart @@ -0,0 +1,361 @@ +// ignore_for_file: invalid_use_of_protected_member + +import 'package:flutter/src/widgets/framework.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/mockito.dart'; +import 'package:provider/provider.dart'; + +import 'common.dart'; + +void main() { + group('DelegateWidget', () { + testWidgets( + "can't call context.inheritFromWidgetOfExactType from first initDelegate", + (tester) async { + await tester.pumpWidget(Provider.value( + value: 42, + child: TestDelegateWidget( + delegate: InitDelegate(), + child: Container(), + ), + )); + + expect(tester.takeException(), isFlutterError); + }); + testWidgets( + "can't call context.inheritFromWidgetOfExactType from initDelegate after an update", + (tester) async { + await tester.pumpWidget(Provider.value( + value: 42, + child: TestDelegateWidget( + delegate: SingleValueDelegate(42), + child: Container(), + ), + )); + + expect(tester.takeException(), isNull); + + await tester.pumpWidget(Provider.value( + value: 42, + child: TestDelegateWidget( + delegate: InitDelegate(), + child: Container(), + ), + )); + + expect(tester.takeException(), isFlutterError); + }); + testWidgets('mount initializes setState and context and calls initDelegate', + (tester) async { + final state = MockStateDelegate(); + final key = GlobalKey(); + + expect(state.context, isNull); + expect(state.setState, isNull); + verifyZeroInteractions(state.initDelegateMock); + + await tester.pumpWidget(TestDelegateWidget( + key: key, + delegate: state, + child: Container(), + )); + + expect(state.context, key.currentContext); + expect(state.setState, key.currentState.setState); + + verify(state.initDelegateMock( + key.currentContext, + key.currentState.setState, + )).called(1); + verifyZeroInteractions(state.didUpdateDelegateMock); + verifyZeroInteractions(state.disposeMock); + }); + testWidgets( + 'rebuilding with delegate of the same type calls didUpdateDelegate', + (tester) async { + final state = MockStateDelegate(); + final state2 = MockStateDelegate(); + final key = GlobalKey(); + + await tester.pumpWidget(TestDelegateWidget( + key: key, + delegate: state, + child: Container(), + )); + clearInteractions(state.initDelegateMock); + + final context = key.currentContext; + final setState = key.currentState.setState; + + await tester.pumpWidget(TestDelegateWidget( + key: key, + delegate: state2, + child: Container(), + )); + + expect(state.context, isNull); + expect(state.setState, isNull); + verifyZeroInteractions(state.initDelegateMock); + verifyZeroInteractions(state.didUpdateDelegateMock); + verifyZeroInteractions(state.disposeMock); + + expect(state2.context, context); + expect(state2.setState, setState); + verify(state2.didUpdateDelegate(state)).called(1); + verifyZeroInteractions(state2.initDelegateMock); + verifyNoMoreInteractions(state2.didUpdateDelegateMock); + verifyZeroInteractions(state2.disposeMock); + }); + testWidgets( + 'rebuilding with delegate of a different type disposes the previous and init the new one', + (tester) async { + final state = MockStateDelegate(); + final state2 = MockStateDelegate(); + final key = GlobalKey(); + + await tester.pumpWidget(TestDelegateWidget( + key: key, + delegate: state, + child: Container(), + )); + clearInteractions(state.initDelegateMock); + + final context = key.currentContext; + final setState = key.currentState.setState; + + await tester.pumpWidget(TestDelegateWidget( + key: key, + delegate: state2, + child: Container(), + )); + + expect(state.context, isNull); + expect(state.setState, isNull); + + verifyZeroInteractions(state.initDelegateMock); + verifyZeroInteractions(state.didUpdateDelegateMock); + verify(state.disposeMock(context, setState)).called(1); + verifyNoMoreInteractions(state.disposeMock); + + expect(state2.context, key.currentContext); + expect(state2.setState, key.currentState.setState); + + verify(state2.initDelegateMock(context, setState)).called(1); + verifyNoMoreInteractions(state2.initDelegateMock); + verifyNoMoreInteractions(state2.didUpdateDelegateMock); + verifyZeroInteractions(state2.disposeMock); + }); + + testWidgets('unmounting the widget calls delegate.dispose', (tester) async { + final state = MockStateDelegate(); + final key = GlobalKey(); + + await tester.pumpWidget(TestDelegateWidget( + key: key, + delegate: state, + child: Container(), + )); + clearInteractions(state.initDelegateMock); + + final context = key.currentContext; + final setState = key.currentState.setState; + + await tester.pumpWidget(Container()); + + expect(state.context, isNull); + expect(state.setState, isNull); + verifyZeroInteractions(state.initDelegateMock); + verifyZeroInteractions(state.didUpdateDelegateMock); + verify(state.disposeMock(context, setState)).called(1); + verifyNoMoreInteractions(state.disposeMock); + }); + + test('throws if delegate is null', () { + expect( + () => TestDelegateWidget(child: Container()), + throwsAssertionError, + ); + }); + }); + + group('SingleValueDelegate', () { + test('implements ValueStateDelegate', () { + expect( + SingleValueDelegate(0), + isInstanceOf>(), + ); + }); + + testWidgets('stores and update value', (tester) async { + int value; + BuildContext context; + final key = GlobalKey(); + + await tester.pumpWidget(BuilderDelegateWidget>( + key: key, + delegate: SingleValueDelegate(0), + builder: (c, d) { + value = d.value; + context = c; + return Container(); + }, + )); + + expect(context, equals(key.currentContext)); + expect(value, equals(0)); + + await tester.pumpWidget(BuilderDelegateWidget>( + key: key, + delegate: SingleValueDelegate(42), + builder: (c, d) { + value = d.value; + context = c; + return Container(); + }, + )); + + expect(context, equals(key.currentContext)); + expect(value, equals(42)); + }); + }); + + group('BuilderStateDelegate', () { + test('implements ValueStateDelegate', () { + expect( + BuilderStateDelegate((_) => 42), + isInstanceOf>(), + ); + }); + test('throws if builder is missing', () { + expect( + () => BuilderStateDelegate(null), + throwsAssertionError, + ); + }); + + testWidgets('initialize value and never recreate it', (tester) async { + int value; + BuildContext context; + final key = GlobalKey(); + + await tester + .pumpWidget(BuilderDelegateWidget>( + key: key, + delegate: BuilderStateDelegate((_) => 42), + builder: (c, d) { + value = d.value; + context = c; + return Container(); + }, + )); + + expect(context, equals(key.currentContext)); + expect(value, equals(42)); + + await tester + .pumpWidget(BuilderDelegateWidget>( + key: key, + delegate: BuilderStateDelegate((_) => 0), + builder: (c, d) { + value = d.value; + context = c; + return Container(); + }, + )); + + expect(context, equals(key.currentContext)); + expect(value, equals(42)); + }); + + testWidgets('initialize value and never recreate it', (tester) async { + final disposeMock = DisposerMock(); + final key = GlobalKey(); + final delegate2 = BuilderStateDelegate( + (_) => 42, + dispose: disposeMock, + ); + + await tester + .pumpWidget(BuilderDelegateWidget>( + key: key, + delegate: delegate2, + builder: (_, __) => Container(), + )); + + final context = key.currentContext; + + verifyZeroInteractions(disposeMock); + + await tester.pumpWidget(Container()); + + verify(disposeMock(context, 42)).called(1); + verifyNoMoreInteractions(disposeMock); + }); + }); +} + +class InitDelegate extends StateDelegate { + @override + void initDelegate() { + super.initDelegate(); + Provider.of(context); + } +} + +class InitDelegateMock extends Mock { + void call(BuildContext context, StateSetter setState); +} + +class DidUpdateDelegateMock extends Mock { + void call(StateDelegate old); +} + +class DisposeMock extends Mock { + void call(BuildContext context, StateSetter setState); +} + +class MockStateDelegate extends StateDelegate { + final disposeMock = DisposeMock(); + final initDelegateMock = InitDelegateMock(); + final didUpdateDelegateMock = DidUpdateDelegateMock(); + + @override + void initDelegate() { + super.initDelegate(); + initDelegateMock(context, setState); + } + + @override + void didUpdateDelegate(StateDelegate old) { + super.didUpdateDelegate(old); + didUpdateDelegateMock(old); + } + + @override + void dispose() { + disposeMock(context, setState); + super.dispose(); + } +} + +class BuilderDelegateWidget> + extends ValueDelegateWidget { + BuilderDelegateWidget({Key key, this.builder, T delegate}) + : super(key: key, delegate: delegate); + + final Widget Function(BuildContext context, T delegate) builder; + + @override + Widget build(BuildContext context) => builder(context, delegate as T); +} + +class TestDelegateWidget extends DelegateWidget { + TestDelegateWidget({Key key, this.child, StateDelegate delegate}) + : super(key: key, delegate: delegate); + + final Widget child; + + @override + Widget build(BuildContext context) => child; +} diff --git a/packages/provider/packages/provider/test/future_provider_test.dart b/packages/provider/packages/provider/test/future_provider_test.dart new file mode 100644 index 0000000..cc08f37 --- /dev/null +++ b/packages/provider/packages/provider/test/future_provider_test.dart @@ -0,0 +1,325 @@ +import 'dart:async'; + +import 'package:flutter/widgets.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/mockito.dart'; +import 'package:provider/provider.dart'; + +import 'common.dart'; + +// tests forked from stream_provider_test.dart +// by replacing Stream with Future and StreamController with Completer + +class ErrorBuilderMock extends Mock { + T call(BuildContext context, Object error); +} + +class MockFuture extends Mock implements Future {} + +void main() { + group('FutureProvider', () { + testWidgets('update when value change', (tester) async { + final completer = Completer(); + final key = GlobalKey(); + + await tester.pumpWidget(FutureProvider.value( + value: completer.future, + child: Container(key: key), + )); + + expect(Provider.of(key.currentContext), null); + + completer.complete(0); + // futures are asynchronous so we have to delay the pump + await Future.microtask(tester.pump); + + expect(Provider.of(key.currentContext), 0); + }); + + testWidgets("don't notify descendants when rebuilding by default", + (tester) async { + final completer = Completer(); + + final builder = BuilderMock(); + when(builder(any)).thenAnswer((invocation) { + final context = invocation.positionalArguments.first as BuildContext; + Provider.of(context); + return Container(); + }); + final child = Builder(builder: builder); + + await tester.pumpWidget(FutureProvider.value( + value: completer.future, + child: child, + )); + + await tester.pumpWidget(FutureProvider.value( + value: completer.future, + child: child, + )); + + verify(builder(any)).called(1); + }); + + testWidgets('pass down keys', (tester) async { + final completer = Completer(); + final key = GlobalKey(); + + await tester.pumpWidget(FutureProvider.value( + key: key, + value: completer.future, + child: Container(), + )); + + expect(key.currentWidget, isInstanceOf()); + }); + + testWidgets('pass updateShouldNotify', (tester) async { + final shouldNotify = UpdateShouldNotifyMock(); + when(shouldNotify(null, 1)).thenReturn(true); + + final completer = Completer(); + await tester.pumpWidget(FutureProvider.value( + value: completer.future, + updateShouldNotify: shouldNotify, + child: Container(), + )); + + verifyZeroInteractions(shouldNotify); + + completer.complete(1); + // futures are asynchronous so we have to delay the pump + await Future.microtask(tester.pump); + + verify(shouldNotify(null, 1)).called(1); + verifyNoMoreInteractions(shouldNotify); + }); + + testWidgets("don't listen future again if it doesn't change", + (tester) async { + final future = MockFuture(); + await tester.pumpWidget(FutureProvider.value( + value: future, + child: Container(), + )); + await tester.pumpWidget(FutureProvider.value( + value: future, + child: Container(), + )); + + verify(future.then(any, onError: anyNamed('onError'))).called(1); + verifyNoMoreInteractions(future); + }); + + testWidgets('future emits error and catchError is missing', (tester) async { + final completer = Completer(); + + await tester.pumpWidget(FutureProvider.value( + value: completer.future, + child: Container(), + )); + + completer.completeError(42); + + await Future.microtask(tester.pump); + final exception = tester.takeException() as Object; + expect(exception, isFlutterError); + expect(exception.toString(), equals(''' +An exception was throw by Future listened by +FutureProvider, but no `catchError` was provided. + +Exception: +42 +''')); + }); + testWidgets('calls catchError if future emits error', (tester) async { + final completer = Completer(); + final key = GlobalKey(); + final catchError = ErrorBuilderMock(); + when(catchError(any, 42)).thenReturn(0); + + await tester.pumpWidget(FutureProvider.value( + value: completer.future, + catchError: catchError, + child: Container(key: key), + )); + + completer.completeError(42); + + await Future.microtask(tester.pump); + + expect(Provider.of(key.currentContext), 0); + + final context = findElementOfWidget>(); + + verify(catchError(context, 42)); + }); + + testWidgets('works with MultiProvider', (tester) async { + final key = GlobalKey(); + await tester.pumpWidget(MultiProvider( + providers: [ + FutureProvider.value(value: Future.value()), + ], + child: Container(key: key), + )); + + expect(Provider.of(key.currentContext), null); + }); + test('works with MultiProvider #2', () { + final provider = FutureProvider.value( + value: Future.value(), + initialData: 42, + child: Container(), + catchError: (_, __) => 42, + key: const Key('42'), + updateShouldNotify: (_, __) => true, + ); + var child2 = Container(); + final clone = provider.cloneWithChild(child2); + + expect(clone.child, equals(child2)); + expect(clone.updateShouldNotify, equals(provider.updateShouldNotify)); + expect(clone.key, equals(provider.key)); + expect(clone.initialData, equals(provider.initialData)); + // ignore: invalid_use_of_protected_member + expect(clone.delegate, equals(provider.delegate)); + expect(clone.catchError, equals(provider.catchError)); + }); + test('works with MultiProvider #3', () { + final provider = FutureProvider( + builder: (_) => Future.value(), + initialData: 42, + child: Container(), + catchError: (_, __) => 42, + key: const Key('42'), + updateShouldNotify: (_, __) => true, + ); + var child2 = Container(); + final clone = provider.cloneWithChild(child2); + + expect(clone.child, equals(child2)); + expect(clone.updateShouldNotify, equals(provider.updateShouldNotify)); + expect(clone.key, equals(provider.key)); + expect(clone.initialData, equals(provider.initialData)); + // ignore: invalid_use_of_protected_member + expect(clone.delegate, equals(provider.delegate)); + expect(clone.catchError, equals(provider.catchError)); + }); + testWidgets('works with null', (tester) async { + final key = GlobalKey(); + await tester.pumpWidget(FutureProvider.value( + value: null, + child: Container(key: key), + )); + + expect(Provider.of(key.currentContext), null); + }); + + group('stateful constructor', () { + test('crashes if builder is null', () { + expect( + () => FutureProvider(builder: null), + throwsAssertionError, + ); + }); + + testWidgets('works with null', (tester) async { + final key = GlobalKey(); + await tester.pumpWidget(FutureProvider( + builder: (_) => null, + child: Container(key: key), + )); + + expect(Provider.of(key.currentContext), null); + + await tester.pumpWidget(Container()); + }); + + testWidgets('create future with builder', (tester) async { + final completer = Completer(); + + final builder = ValueBuilderMock>(); + when(builder(any)).thenAnswer((_) => completer.future); + + await tester.pumpWidget(FutureProvider( + builder: builder, + child: Container(), + )); + + final context = findElementOfWidget>(); + + verify(builder(context)).called(1); + + // extra build to see if builder isn't called again + await tester.pumpWidget(FutureProvider( + builder: builder, + child: Container(), + )); + + await tester.pumpWidget(Container()); + + verifyNoMoreInteractions(builder); + }); + + testWidgets('pass updateShouldNotify', (tester) async { + final shouldNotify = UpdateShouldNotifyMock(); + when(shouldNotify(null, 1)).thenReturn(true); + + var completer = Completer(); + await tester.pumpWidget(FutureProvider( + builder: (_) => completer.future, + updateShouldNotify: shouldNotify, + child: Container(), + )); + + verifyZeroInteractions(shouldNotify); + + completer.complete(1); + // futures are asynchronous so we have to delay the pump + await Future.microtask(tester.pump); + + verify(shouldNotify(null, 1)).called(1); + verifyNoMoreInteractions(shouldNotify); + }); + + testWidgets( + 'Changing from default to stateful constructor calls stateful builder', + (tester) async { + final key = GlobalKey(); + final completer = Completer(); + await tester.pumpWidget(FutureProvider.value( + value: completer.future, + child: Container(), + )); + + await tester.pumpWidget(FutureProvider( + builder: (_) => Future.value(42), + child: Container(key: key), + )); + + await tester.pump(); + + expect(Provider.of(key.currentContext), 42); + + await tester.pumpWidget(Container()); + }); + testWidgets('Changing from stateful to default constructor', + (tester) async { + await tester.pumpWidget(FutureProvider( + builder: (_) => Future.value(0), + child: Container(), + )); + + final key = GlobalKey(); + await tester.pumpWidget(FutureProvider.value( + value: Future.value(1), + child: Container(key: key), + )); + await tester.pump(); + + expect(Provider.of(key.currentContext), 1); + }); + }); + }); +} diff --git a/packages/provider/packages/provider/test/listenable_provider_test.dart b/packages/provider/packages/provider/test/listenable_provider_test.dart new file mode 100644 index 0000000..ebbe3b1 --- /dev/null +++ b/packages/provider/packages/provider/test/listenable_provider_test.dart @@ -0,0 +1,375 @@ +import 'package:flutter/widgets.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/mockito.dart'; +import 'package:provider/provider.dart'; +import 'package:flutter/foundation.dart'; + +import 'common.dart'; + +void main() { + group('ListenableProvider', () { + testWidgets('works with MultiProvider', (tester) async { + final key = GlobalKey(); + var listenable = ChangeNotifier(); + + await tester.pumpWidget(MultiProvider( + providers: [ + ListenableProvider.value(value: listenable), + ], + child: Container(key: key), + )); + + expect(Provider.of(key.currentContext), listenable); + }); + test('works with MultiProvider #2', () { + final provider = ListenableProvider.value( + key: const Key('42'), + value: ChangeNotifier(), + child: Container(), + ); + var child2 = Container(); + final clone = provider.cloneWithChild(child2); + + expect(clone.child, equals(child2)); + expect(clone.key, equals(provider.key)); + // ignore: invalid_use_of_protected_member + expect(clone.delegate, equals(provider.delegate)); + }); + test('works with MultiProvider #3', () { + final provider = ListenableProvider( + builder: (_) => ChangeNotifier(), + dispose: (_, n) {}, + child: Container(), + key: const Key('42'), + ); + var child2 = Container(); + final clone = provider.cloneWithChild(child2); + + expect(clone.child, equals(child2)); + expect(clone.key, equals(provider.key)); + // ignore: invalid_use_of_protected_member + expect(clone.delegate, equals(provider.delegate)); + }); + + group('value constructor', () { + testWidgets('pass down key', (tester) async { + final listenable = ChangeNotifier(); + final keyProvider = GlobalKey(); + + await tester.pumpWidget(ListenableProvider.value( + key: keyProvider, + value: listenable, + child: Container(), + )); + expect( + keyProvider.currentWidget, + isNotNull, + ); + }); + }); + testWidgets("don't listen again if listenable instance doesn't change", + (tester) async { + final listenable = MockNotifier(); + await tester.pumpWidget(ListenableProvider.value( + value: listenable, + child: Container(), + )); + await tester.pumpWidget(ListenableProvider.value( + value: listenable, + child: Container(), + )); + + verify(listenable.addListener(any)).called(1); + verifyNoMoreInteractions(listenable); + }); + testWidgets('works with null (default)', (tester) async { + final key = GlobalKey(); + await tester.pumpWidget(ListenableProvider.value( + value: null, + child: Container(key: key), + )); + + expect(Provider.of(key.currentContext), null); + }); + testWidgets('works with null (builder)', (tester) async { + final key = GlobalKey(); + await tester.pumpWidget(ListenableProvider( + builder: (_) => null, + child: Container(key: key), + )); + + expect(Provider.of(key.currentContext), null); + }); + group('stateful constructor', () { + testWidgets('called with context', (tester) async { + final builder = ValueBuilderMock(); + final key = GlobalKey(); + + await tester.pumpWidget(ListenableProvider( + key: key, + builder: builder, + child: Container(), + )); + verify(builder(key.currentContext)).called(1); + }); + test('throws if builder is null', () { + expect( + // ignore: prefer_const_constructors + () => ListenableProvider( + builder: null, + ), + throwsAssertionError, + ); + }); + testWidgets('pass down key', (tester) async { + final keyProvider = GlobalKey(); + + await tester.pumpWidget(ListenableProvider( + key: keyProvider, + builder: (_) => ChangeNotifier(), + child: Container(), + )); + expect( + keyProvider.currentWidget, + isNotNull, + ); + }); + }); + testWidgets('stateful builder called once', (tester) async { + final listenable = MockNotifier(); + final builder = ValueBuilderMock(); + when(builder(any)).thenReturn(listenable); + + await tester.pumpWidget(ListenableProvider( + builder: builder, + child: Container(), + )); + + final context = findElementOfWidget(); + + verify(builder(context)).called(1); + verifyNoMoreInteractions(builder); + clearInteractions(listenable); + + await tester.pumpWidget(ListenableProvider( + builder: builder, + child: Container(), + )); + + verifyNoMoreInteractions(builder); + verifyNoMoreInteractions(listenable); + }); + testWidgets('dispose called on unmount', (tester) async { + final listenable = MockNotifier(); + final builder = ValueBuilderMock(); + final disposer = DisposerMock(); + when(builder(any)).thenReturn(listenable); + + await tester.pumpWidget(ListenableProvider( + builder: builder, + dispose: disposer, + child: Container(), + )); + + final context = findElementOfWidget(); + + verify(builder(context)).called(1); + verifyNoMoreInteractions(builder); + final listener = verify(listenable.addListener(captureAny)).captured.first + as VoidCallback; + clearInteractions(listenable); + + await tester.pumpWidget(Container()); + + verifyInOrder([ + listenable.removeListener(listener), + disposer(context, listenable), + ]); + verifyNoMoreInteractions(builder); + verifyNoMoreInteractions(listenable); + }); + testWidgets('dispose can be null', (tester) async { + await tester.pumpWidget(ListenableProvider( + builder: (_) => ChangeNotifier(), + child: Container(), + )); + + await tester.pumpWidget(Container()); + }); + testWidgets( + 'Changing from default to stateful constructor calls stateful builder', + (tester) async { + final listenable = MockNotifier(); + var listenable2 = ChangeNotifier(); + final key = GlobalKey(); + await tester.pumpWidget(ListenableProvider.value( + value: listenable, + child: Container(), + )); + final listener = verify(listenable.addListener(captureAny)).captured.first + as VoidCallback; + clearInteractions(listenable); + + await tester.pumpWidget(ListenableProvider( + builder: (_) { + return listenable2; + }, + child: Container(key: key), + )); + + expect(Provider.of(key.currentContext), listenable2); + + await tester.pumpWidget(Container()); + verify(listenable.removeListener(listener)).called(1); + verifyNoMoreInteractions(listenable); + }); + testWidgets( + 'Changing from stateful to default constructor dispose correctly stateful listenable', + (tester) async { + final ChangeNotifier listenable = MockNotifier(); + final disposer = DisposerMock(); + var listenable2 = ChangeNotifier(); + final key = GlobalKey(); + + await tester.pumpWidget(ListenableProvider( + builder: (_) => listenable, + dispose: disposer, + child: Container(), + )); + + final context = findElementOfWidget>(); + + final listener = verify(listenable.addListener(captureAny)).captured.first + as VoidCallback; + clearInteractions(listenable); + await tester.pumpWidget(ListenableProvider.value( + value: listenable2, + child: Container(key: key), + )); + + expect(Provider.of(key.currentContext), listenable2); + + await tester.pumpWidget(Container()); + + verifyInOrder([ + listenable.removeListener(listener), + disposer(context, listenable), + ]); + verifyNoMoreInteractions(listenable); + }); + + testWidgets('changing listenable rebuilds descendants', (tester) async { + final builder = BuilderMock(); + when(builder(any)).thenReturn(Container()); + + var listenable = ChangeNotifier(); + Widget build() { + return ListenableProvider.value( + value: listenable, + child: Builder(builder: (context) { + Provider.of(context); + return builder(context); + }), + ); + } + + await tester.pumpWidget(build()); + + verify(builder(any)).called(1); + + // ignore: invalid_use_of_protected_member + expect(listenable.hasListeners, true); + + var previousNotifier = listenable; + listenable = ChangeNotifier(); + await tester.pumpWidget(build()); + + // ignore: invalid_use_of_protected_member + expect(listenable.hasListeners, true); + // ignore: invalid_use_of_protected_member + expect(previousNotifier.hasListeners, false); + + verify(builder(any)).called(1); + + await tester.pumpWidget(Container()); + + // ignore: invalid_use_of_protected_member + expect(listenable.hasListeners, false); + }); + testWidgets("rebuilding with the same provider don't rebuilds descendants", + (tester) async { + final listenable = ChangeNotifier(); + final keyChild = GlobalKey(); + final builder = BuilderMock(); + when(builder(any)).thenReturn(Container()); + + final child = Builder( + key: keyChild, + builder: builder, + ); + + await tester.pumpWidget(ListenableProvider.value( + value: listenable, + child: child, + )); + + verify(builder(any)).called(1); + expect(Provider.of(keyChild.currentContext), listenable); + + await tester.pumpWidget(ListenableProvider.value( + value: listenable, + child: child, + )); + verifyNoMoreInteractions(builder); + expect(Provider.of(keyChild.currentContext), listenable); + + listenable.notifyListeners(); + await tester.pump(); + + verify(builder(any)).called(1); + expect(Provider.of(keyChild.currentContext), listenable); + + await tester.pumpWidget(ListenableProvider.value( + value: listenable, + child: child, + )); + verifyNoMoreInteractions(builder); + expect(Provider.of(keyChild.currentContext), listenable); + + await tester.pumpWidget(ListenableProvider.value( + value: listenable, + child: child, + )); + verifyNoMoreInteractions(builder); + expect(Provider.of(keyChild.currentContext), listenable); + }); + testWidgets('notifylistener rebuilds descendants', (tester) async { + final listenable = ChangeNotifier(); + final keyChild = GlobalKey(); + final builder = BuilderMock(); + when(builder(any)).thenReturn(Container()); + + final child = Builder( + key: keyChild, + builder: (context) { + // subscribe + Provider.of(context); + return builder(context); + }, + ); + var changeNotifierProvider = ListenableProvider.value( + value: listenable, + child: child, + ); + await tester.pumpWidget(changeNotifierProvider); + + clearInteractions(builder); + // ignore: invalid_use_of_protected_member + listenable.notifyListeners(); + await Future.value(); + await tester.pump(); + verify(builder(any)).called(1); + expect(Provider.of(keyChild.currentContext), listenable); + }); + }); +} diff --git a/packages/provider/packages/provider/test/listenable_proxy_provider_test.dart b/packages/provider/packages/provider/test/listenable_proxy_provider_test.dart new file mode 100644 index 0000000..0e3a2bb --- /dev/null +++ b/packages/provider/packages/provider/test/listenable_proxy_provider_test.dart @@ -0,0 +1,246 @@ +import 'package:flutter/widgets.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/mockito.dart'; +import 'package:provider/provider.dart'; +import 'package:provider/src/proxy_provider.dart' show ProxyProviderBase; + +import 'common.dart'; + +class _ListenableCombined = Combined with ChangeNotifier; + +void main() { + final a = A(); + final b = B(); + final c = C(); + final d = D(); + final e = E(); + final f = F(); + + final combinedConsumerMock = ConsumerBuilderMock(); + setUp(() => when(combinedConsumerMock(any)).thenReturn(Container())); + tearDown(() { + clearInteractions(combinedConsumerMock); + }); + + final mockConsumer = Consumer<_ListenableCombined>( + builder: (context, combined, child) => combinedConsumerMock(combined), + ); + + group('ListenableProxyProvider', () { + test('throws if builder is missing', () { + expect( + () => ListenableProxyProvider(builder: null), + throwsAssertionError, + ); + }); + + testWidgets('rebuilds dependendents when listeners are called', + (tester) async { + final notifier = ValueNotifier(0); + await tester.pumpWidget( + MultiProvider( + providers: [ + Provider.value(value: 0), + ListenableProxyProvider>( + initialBuilder: (_) => notifier, + builder: (_, count, value) => value..value = count, + ) + ], + child: Consumer>(builder: (_, value, __) { + return Text( + value.value.toString(), + textDirection: TextDirection.ltr, + ); + }), + ), + ); + + expect(find.text('0'), findsOneWidget); + expect(find.text('1'), findsNothing); + + notifier.value++; + await tester.pump(); + + expect(find.text('1'), findsOneWidget); + expect(find.text('0'), findsNothing); + }); + testWidgets('disposes of created value', (tester) async { + final dispose = DisposerMock>(); + final notifier = ValueNotifier(0); + final key = GlobalKey(); + + await tester.pumpWidget( + MultiProvider( + providers: [ + Provider.value(value: 0), + ListenableProxyProvider>( + key: key, + initialBuilder: (_) => notifier, + builder: (_, count, value) => value..value = count, + dispose: dispose, + ) + ], + child: Container(), + ), + ); + + final context = key.currentContext; + verifyZeroInteractions(dispose); + + await tester.pumpWidget(Container()); + + verify(dispose(context, notifier)).called(1); + verifyNoMoreInteractions(dispose); + }); + }); + + group('ListenableProxyProvider variants', () { + Finder findProxyProvider() => find + .byWidgetPredicate((widget) => widget is ProxyProviderBase); + testWidgets('ListenableProxyProvider2', (tester) async { + await tester.pumpWidget( + MultiProvider( + providers: [ + Provider.value(value: a), + Provider.value(value: b), + Provider.value(value: c), + Provider.value(value: d), + Provider.value(value: e), + Provider.value(value: f), + ListenableProxyProvider2( + initialBuilder: (_) => _ListenableCombined(null, null, null), + builder: (context, a, b, previous) => + _ListenableCombined(context, previous, a, b), + ) + ], + child: mockConsumer, + ), + ); + + final context = tester.element(findProxyProvider()); + + verify( + combinedConsumerMock( + _ListenableCombined( + context, _ListenableCombined(null, null, null), a, b), + ), + ).called(1); + }); + testWidgets('ListenableProxyProvider3', (tester) async { + await tester.pumpWidget( + MultiProvider( + providers: [ + Provider.value(value: a), + Provider.value(value: b), + Provider.value(value: c), + Provider.value(value: d), + Provider.value(value: e), + Provider.value(value: f), + ListenableProxyProvider3( + initialBuilder: (_) => _ListenableCombined(null, null, null), + builder: (context, a, b, c, previous) => + _ListenableCombined(context, previous, a, b, c), + ) + ], + child: mockConsumer, + ), + ); + + final context = tester.element(findProxyProvider()); + + verify( + combinedConsumerMock( + _ListenableCombined( + context, _ListenableCombined(null, null, null), a, b, c), + ), + ).called(1); + }); + testWidgets('ListenableProxyProvider4', (tester) async { + await tester.pumpWidget( + MultiProvider( + providers: [ + Provider.value(value: a), + Provider.value(value: b), + Provider.value(value: c), + Provider.value(value: d), + Provider.value(value: e), + Provider.value(value: f), + ListenableProxyProvider4( + initialBuilder: (_) => _ListenableCombined(null, null, null), + builder: (context, a, b, c, d, previous) => + _ListenableCombined(context, previous, a, b, c, d), + ) + ], + child: mockConsumer, + ), + ); + + final context = tester.element(findProxyProvider()); + + verify( + combinedConsumerMock( + _ListenableCombined( + context, _ListenableCombined(null, null, null), a, b, c, d), + ), + ).called(1); + }); + testWidgets('ListenableProxyProvider5', (tester) async { + await tester.pumpWidget( + MultiProvider( + providers: [ + Provider.value(value: a), + Provider.value(value: b), + Provider.value(value: c), + Provider.value(value: d), + Provider.value(value: e), + Provider.value(value: f), + ListenableProxyProvider5( + initialBuilder: (_) => _ListenableCombined(null, null, null), + builder: (context, a, b, c, d, e, previous) => + _ListenableCombined(context, previous, a, b, c, d, e), + ) + ], + child: mockConsumer, + ), + ); + + final context = tester.element(findProxyProvider()); + + verify( + combinedConsumerMock( + _ListenableCombined(context, _ListenableCombined(null, null, null), a, + b, c, d, e, null), + ), + ).called(1); + }); + testWidgets('ListenableProxyProvider6', (tester) async { + await tester.pumpWidget( + MultiProvider( + providers: [ + Provider.value(value: a), + Provider.value(value: b), + Provider.value(value: c), + Provider.value(value: d), + Provider.value(value: e), + Provider.value(value: f), + ListenableProxyProvider6( + initialBuilder: (_) => _ListenableCombined(null, null, null), + builder: (context, a, b, c, d, e, f, previous) => + _ListenableCombined(context, previous, a, b, c, d, e, f), + ) + ], + child: mockConsumer, + ), + ); + + final context = tester.element(findProxyProvider()); + + verify( + combinedConsumerMock( + _ListenableCombined( + context, _ListenableCombined(null, null, null), a, b, c, d, e, f), + ), + ).called(1); + }); + }); +} diff --git a/packages/provider/packages/provider/test/multi_provider_test.dart b/packages/provider/packages/provider/test/multi_provider_test.dart new file mode 100644 index 0000000..1fa24b3 --- /dev/null +++ b/packages/provider/packages/provider/test/multi_provider_test.dart @@ -0,0 +1,111 @@ +import 'package:flutter/widgets.dart' hide TypeMatcher; +import 'package:flutter_test/flutter_test.dart'; +import 'package:matcher/matcher.dart'; +import 'package:provider/provider.dart'; + +Type _typeOf() => T; + +Matcher throwsProviderNotFound({Type widgetType, Type valueType}) { + return throwsA(const TypeMatcher() + .having((err) => err.valueType, 'valueType', valueType) + .having((err) => err.widgetType, 'widgetType', widgetType)); +} + +void main() { + group('MultiProvider', () { + test('cloneWithChild works', () { + final provider = MultiProvider( + providers: [], + child: Container(), + key: const ValueKey(42), + ); + + final newChild = Container(); + final clone = provider.cloneWithChild(newChild); + expect(clone.child, newChild); + expect(clone.providers, provider.providers); + expect(clone.key, provider.key); + }); + test('throw if providers is null', () { + expect( + () => MultiProvider(providers: null, child: Container()), + throwsAssertionError, + ); + }); + + testWidgets('MultiProvider with empty providers returns child', + (tester) async { + await tester.pumpWidget(const MultiProvider( + providers: [], + child: Text( + 'Foo', + textDirection: TextDirection.ltr, + ), + )); + + expect(find.text('Foo'), findsOneWidget); + }); + + testWidgets('MultiProvider children can only access parent providers', + (tester) async { + final k1 = GlobalKey(); + final k2 = GlobalKey(); + final k3 = GlobalKey(); + final p1 = Provider.value(key: k1, value: 42); + final p2 = Provider.value(key: k2, value: 'foo'); + final p3 = Provider.value(key: k3, value: 44.0); + + final keyChild = GlobalKey(); + await tester.pumpWidget(MultiProvider( + providers: [p1, p2, p3], + child: Text('Foo', key: keyChild, textDirection: TextDirection.ltr), + )); + + expect(find.text('Foo'), findsOneWidget); + + // p1 cannot access to p1/p2/p3 + expect( + () => Provider.of(k1.currentContext), + throwsProviderNotFound( + valueType: int, widgetType: _typeOf>()), + ); + expect( + () => Provider.of(k1.currentContext), + throwsProviderNotFound( + valueType: String, widgetType: _typeOf>()), + ); + expect( + () => Provider.of(k1.currentContext), + throwsProviderNotFound( + valueType: double, widgetType: _typeOf>()), + ); + + // p2 can access only p1 + expect(Provider.of(k2.currentContext), 42); + expect( + () => Provider.of(k2.currentContext), + throwsProviderNotFound( + valueType: String, widgetType: _typeOf>()), + ); + expect( + () => Provider.of(k2.currentContext), + throwsProviderNotFound( + valueType: double, widgetType: _typeOf>()), + ); + + // p3 can access both p1 and p2 + expect(Provider.of(k3.currentContext), 42); + expect(Provider.of(k3.currentContext), 'foo'); + expect( + () => Provider.of(k3.currentContext), + throwsProviderNotFound( + valueType: double, widgetType: _typeOf>()), + ); + + // the child can access them all + expect(Provider.of(keyChild.currentContext), 42); + expect(Provider.of(keyChild.currentContext), 'foo'); + expect(Provider.of(keyChild.currentContext), 44); + }); + }); +} diff --git a/packages/provider/packages/provider/test/provider_test.dart b/packages/provider/packages/provider/test/provider_test.dart new file mode 100644 index 0000000..e102b4c --- /dev/null +++ b/packages/provider/packages/provider/test/provider_test.dart @@ -0,0 +1,225 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/widgets.dart' hide TypeMatcher; +import 'package:flutter_test/flutter_test.dart'; +import 'package:provider/provider.dart'; +import 'package:test_api/test_api.dart' show TypeMatcher; + +import 'common.dart'; + +void main() { + group('Provider', () { + testWidgets('throws if the provided value is a Listenable/Stream', + (tester) async { + await tester.pumpWidget( + Provider.value( + value: MyListenable(), + child: Container(), + ), + ); + + expect(tester.takeException(), isFlutterError); + + await tester.pumpWidget( + Provider.value( + value: MyStream(), + child: Container(), + ), + ); + + expect(tester.takeException(), isFlutterError); + }); + testWidgets('debugCheckInvalidValueType can be disabled', (tester) async { + final previous = Provider.debugCheckInvalidValueType; + Provider.debugCheckInvalidValueType = null; + addTearDown(() => Provider.debugCheckInvalidValueType = previous); + + await tester.pumpWidget( + Provider.value( + value: MyListenable(), + child: Container(), + ), + ); + + await tester.pumpWidget( + Provider.value( + value: MyStream(), + child: Container(), + ), + ); + }); + test('cloneWithChild works', () { + final provider = Provider.value( + value: 42, + child: Container(), + key: const ValueKey(42), + updateShouldNotify: (int _, int __) => true, + ); + + final newChild = Container(); + final clone = provider.cloneWithChild(newChild); + expect(clone.child, equals(newChild)); + // ignore: invalid_use_of_protected_member + expect(clone.delegate, equals(provider.delegate)); + expect(clone.key, equals(provider.key)); + expect(provider.updateShouldNotify, equals(clone.updateShouldNotify)); + }); + testWidgets('simple usage', (tester) async { + var buildCount = 0; + int value; + double second; + + // We voluntarily reuse the builder instance so that later call to pumpWidget + // don't call builder again unless subscribed to an inheritedWidget + final builder = Builder( + builder: (context) { + buildCount++; + value = Provider.of(context); + second = Provider.of(context, listen: false); + return Container(); + }, + ); + + await tester.pumpWidget( + Provider.value( + value: 24.0, + child: Provider.value( + value: 42, + child: builder, + ), + ), + ); + + expect(value, equals(42)); + expect(second, equals(24.0)); + expect(buildCount, equals(1)); + + // nothing changed + await tester.pumpWidget( + Provider.value( + value: 24.0, + child: Provider.value( + value: 42, + child: builder, + ), + ), + ); + // didn't rebuild + expect(buildCount, equals(1)); + + // changed a value we are subscribed to + await tester.pumpWidget( + Provider.value( + value: 24.0, + child: Provider.value( + value: 43, + child: builder, + ), + ), + ); + expect(value, equals(43)); + expect(second, equals(24.0)); + // got rebuilt + expect(buildCount, equals(2)); + + // changed a value we are _not_ subscribed to + await tester.pumpWidget( + Provider.value( + value: 20.0, + child: Provider.value( + value: 43, + child: builder, + ), + ), + ); + // didn't get rebuilt + expect(buildCount, equals(2)); + }); + + testWidgets('throws an error if no provider found', (tester) async { + await tester.pumpWidget(Builder(builder: (context) { + Provider.of(context); + return Container(); + })); + + expect( + tester.takeException(), + const TypeMatcher() + .having((err) => err.valueType, 'valueType', String) + .having((err) => err.widgetType, 'widgetType', Builder) + .having((err) => err.toString(), 'toString()', ''' +Error: Could not find the correct Provider above this Builder Widget + +To fix, please: + + * Ensure the Provider is an ancestor to this Builder Widget + * Provide types to Provider + * Provide types to Consumer + * Provide types to Provider.of() + * Always use package imports. Ex: `import 'package:my_app/my_code.dart'; + * Ensure the correct `context` is being used. + +If none of these solutions work, please file a bug at: +https://github.com/rrousselGit/provider/issues +'''), + ); + }); + + testWidgets('update should notify', (tester) async { + int old; + int curr; + var callCount = 0; + final updateShouldNotify = (int o, int c) { + callCount++; + old = o; + curr = c; + return o != c; + }; + + var buildCount = 0; + int buildValue; + final builder = Builder(builder: (BuildContext context) { + buildValue = Provider.of(context); + buildCount++; + return Container(); + }); + + await tester.pumpWidget( + Provider.value( + value: 24, + updateShouldNotify: updateShouldNotify, + child: builder, + ), + ); + expect(callCount, equals(0)); + expect(buildCount, equals(1)); + expect(buildValue, equals(24)); + + // value changed + await tester.pumpWidget( + Provider.value( + value: 25, + updateShouldNotify: updateShouldNotify, + child: builder, + ), + ); + expect(callCount, equals(1)); + expect(old, equals(24)); + expect(curr, equals(25)); + expect(buildCount, equals(2)); + expect(buildValue, equals(25)); + + // value didnt' change + await tester.pumpWidget( + Provider.value( + value: 25, + updateShouldNotify: updateShouldNotify, + child: builder, + ), + ); + expect(callCount, equals(2)); + expect(old, equals(25)); + expect(curr, equals(25)); + expect(buildCount, equals(2)); + }); + }); +} diff --git a/packages/provider/packages/provider/test/proxy_provider_test.dart b/packages/provider/packages/provider/test/proxy_provider_test.dart new file mode 100644 index 0000000..b1935a1 --- /dev/null +++ b/packages/provider/packages/provider/test/proxy_provider_test.dart @@ -0,0 +1,552 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/mockito.dart'; +import 'package:provider/provider.dart'; +import 'package:provider/src/proxy_provider.dart' + show NumericProxyProvider, Void; + +import 'common.dart'; + +Finder findProvider() => find.byWidgetPredicate( + // comparing `runtimeType` instead of using `is` because `is` accepts subclasses but InheritedWidgets don't. + (widget) => widget.runtimeType == typeOf>()); + +void main() { + final a = A(); + final b = B(); + final c = C(); + final d = D(); + final e = E(); + final f = F(); + + final combinedConsumerMock = ConsumerBuilderMock(); + setUp(() => when(combinedConsumerMock(any)).thenReturn(Container())); + tearDown(() { + clearInteractions(combinedConsumerMock); + }); + + final mockConsumer = Consumer( + builder: (context, combined, child) => combinedConsumerMock(combined), + ); + + group('ProxyProvider', () { + final combiner = CombinerMock(); + setUp(() { + when(combiner(any, any, any)).thenAnswer((Invocation invocation) { + return Combined( + invocation.positionalArguments.first as BuildContext, + invocation.positionalArguments[2] as Combined, + invocation.positionalArguments[1] as A, + ); + }); + }); + tearDown(() => clearInteractions(combiner)); + + Finder findProxyProvider() => find.byWidgetPredicate( + (widget) => widget is NumericProxyProvider, + ); + + testWidgets('throws if the provided value is a Listenable/Stream', + (tester) async { + await tester.pumpWidget( + MultiProvider( + providers: [ + Provider.value(value: a), + ProxyProvider( + builder: (_, __, ___) => MyListenable(), + ) + ], + child: Container(), + ), + ); + + expect(tester.takeException(), isFlutterError); + + await tester.pumpWidget( + MultiProvider( + providers: [ + Provider.value(value: a), + ProxyProvider( + builder: (_, __, ___) => MyStream(), + ) + ], + child: Container(), + ), + ); + + expect(tester.takeException(), isFlutterError); + }); + testWidgets('debugCheckInvalidValueType can be disabled', (tester) async { + final previous = Provider.debugCheckInvalidValueType; + Provider.debugCheckInvalidValueType = null; + addTearDown(() => Provider.debugCheckInvalidValueType = previous); + + await tester.pumpWidget( + MultiProvider( + providers: [ + Provider.value(value: a), + ProxyProvider( + builder: (_, __, ___) => MyListenable(), + ) + ], + child: Container(), + ), + ); + + await tester.pumpWidget( + MultiProvider( + providers: [ + Provider.value(value: a), + ProxyProvider( + builder: (_, __, ___) => MyStream(), + ) + ], + child: Container(), + ), + ); + }); + + testWidgets('initialBuilder creates initial value', (tester) async { + final initialBuilder = ValueBuilderMock(); + final key = GlobalKey(); + + when(initialBuilder(any)).thenReturn(Combined(null, null, null)); + + await tester.pumpWidget( + MultiProvider( + providers: [ + Provider.value(value: a), + ProxyProvider( + key: key, + initialBuilder: initialBuilder, + builder: combiner, + ) + ], + child: mockConsumer, + ), + ); + + final details = verify(initialBuilder(captureAny))..called(1); + expect(details.captured.first, equals(key.currentContext)); + + verify(combiner(key.currentContext, a, Combined(null, null, null))); + }); + testWidgets('consume another providers', (tester) async { + await tester.pumpWidget( + MultiProvider( + providers: [ + Provider.value(value: a), + ProxyProvider( + builder: combiner, + ) + ], + child: mockConsumer, + ), + ); + + final context = tester.element(findProxyProvider()); + + verify(combinedConsumerMock(Combined(context, null, a))).called(1); + verifyNoMoreInteractions(combinedConsumerMock); + + verify(combiner(context, a, null)).called(1); + verifyNoMoreInteractions(combiner); + }); + + test('throws if builder is null', () { + // ignore: prefer_const_constructors + expect(() => ProxyProvider(builder: null), + throwsAssertionError); + }); + + testWidgets('rebuild descendants if value change', (tester) async { + await tester.pumpWidget( + MultiProvider( + providers: [ + Provider.value(value: a), + ProxyProvider( + builder: combiner, + ) + ], + child: mockConsumer, + ), + ); + + final a2 = A(); + + await tester.pumpWidget( + MultiProvider( + providers: [ + Provider.value(value: a2), + ProxyProvider( + builder: combiner, + ) + ], + child: mockConsumer, + ), + ); + final context = tester.element(findProxyProvider()); + + verifyInOrder([ + combiner(context, a, null), + combinedConsumerMock(Combined(context, null, a)), + combiner(context, a2, Combined(context, null, a)), + combinedConsumerMock(Combined(context, Combined(context, null, a), a2)), + ]); + + verifyNoMoreInteractions(combiner); + verifyNoMoreInteractions(combinedConsumerMock); + }); + testWidgets('call dispose when unmounted with the latest result', + (tester) async { + final dispose = DisposerMock(); + final dispose2 = DisposerMock(); + + await tester.pumpWidget( + MultiProvider( + providers: [ + Provider.value(value: a), + ProxyProvider(builder: combiner, dispose: dispose) + ], + child: mockConsumer, + ), + ); + + final a2 = A(); + + // ProxyProvider creates a new Combined instance + await tester.pumpWidget( + MultiProvider( + providers: [ + Provider.value(value: a2), + ProxyProvider(builder: combiner, dispose: dispose2) + ], + child: mockConsumer, + ), + ); + final context = tester.element(findProxyProvider()); + + await tester.pumpWidget(Container()); + + verifyZeroInteractions(dispose); + verify( + dispose2(context, Combined(context, Combined(context, null, a), a2))); + }); + + testWidgets("don't rebuild descendants if value doesn't change", + (tester) async { + await tester.pumpWidget( + MultiProvider( + providers: [ + Provider.value(value: a), + ProxyProvider( + builder: (c, a, p) => combiner(c, a, null), + ) + ], + child: mockConsumer, + ), + ); + + await tester.pumpWidget( + MultiProvider( + providers: [ + Provider.value( + value: a, + updateShouldNotify: (A _, A __) => true, + ), + ProxyProvider( + builder: (c, a, p) { + combiner(c, a, p); + return p; + }, + ) + ], + child: mockConsumer, + ), + ); + final context = tester.element(findProxyProvider()); + + verifyInOrder([ + combiner(context, a, null), + combinedConsumerMock(Combined(context, null, a)), + combiner(context, a, Combined(context, null, a)), + ]); + + verifyNoMoreInteractions(combiner); + verifyNoMoreInteractions(combinedConsumerMock); + }); + + testWidgets('pass down updateShouldNotify', (tester) async { + var buildCount = 0; + final child = Builder(builder: (context) { + buildCount++; + + return Text( + '$buildCount ${Provider.of(context)}', + textDirection: TextDirection.ltr, + ); + }); + + final shouldNotify = UpdateShouldNotifyMock(); + when(shouldNotify('Hello', 'Hello')).thenReturn(false); + + await tester.pumpWidget(MultiProvider( + providers: [ + Provider.value( + value: 'Hello', updateShouldNotify: (_, __) => true), + ProxyProvider( + builder: (_, value, __) => value, + updateShouldNotify: shouldNotify, + ), + ], + child: child, + )); + + await tester.pumpWidget(MultiProvider( + providers: [ + Provider.value( + value: 'Hello', updateShouldNotify: (_, __) => true), + ProxyProvider( + builder: (_, value, __) => value, + updateShouldNotify: shouldNotify, + ), + ], + child: child, + )); + + verify(shouldNotify('Hello', 'Hello')).called(1); + verifyNoMoreInteractions(shouldNotify); + + expect(find.text('2 Hello'), findsNothing); + expect(find.text('1 Hello'), findsOneWidget); + }); + testWidgets('works with MultiProvider', (tester) async { + final key = GlobalKey(); + + await tester.pumpWidget(MultiProvider( + providers: [ + Provider.value(value: a), + ProxyProvider(builder: (c, a, p) => Combined(c, p, a)), + ], + child: Container(key: key), + )); + final context = tester.element(findProxyProvider()); + + expect( + Provider.of(key.currentContext), + Combined(context, null, a), + ); + }); + test('works with MultiProvider #2', () { + final provider = ProxyProvider( + key: const Key('42'), + initialBuilder: (_) => null, + builder: (_, __, ___) => null, + updateShouldNotify: (_, __) => null, + dispose: (_, __) {}, + child: Container(), + ); + var child2 = Container(); + final clone = provider.cloneWithChild(child2); + + expect(clone.child, equals(child2)); + expect(clone.key, equals(provider.key)); + expect(clone.initialBuilder, equals(provider.initialBuilder)); + expect(clone.builder, equals(provider.builder)); + expect(clone.updateShouldNotify, equals(provider.updateShouldNotify)); + expect(clone.dispose, equals(provider.dispose)); + // expect(clone.providerBuilder, equals(provider.providerBuilder)); + }); + + // useful for libraries such as Mobx where events are synchronously dispatched + testWidgets( + 'builder callback can trigger descendants setState synchronously', + (tester) async { + var statefulBuildCount = 0; + void Function(VoidCallback) setState; + + final statefulBuilder = StatefulBuilder(builder: (_, s) { + setState = s; + statefulBuildCount++; + return Container(); + }); + + await tester.pumpWidget(MultiProvider( + providers: [ + Provider.value(value: a), + ProxyProvider(builder: (c, a, p) => Combined(c, p, a)), + ], + child: statefulBuilder, + )); + + await tester.pumpWidget(MultiProvider( + providers: [ + Provider.value(value: A()), + ProxyProvider(builder: (c, a, p) { + setState(() {}); + return Combined(c, p, a); + }), + ], + child: statefulBuilder, + )); + + expect( + statefulBuildCount, + 2, + reason: 'builder must not be called asynchronously', + ); + }); + }); + + group('ProxyProvider variants', () { + Finder findProxyProvider() => find.byWidgetPredicate( + (widget) => + widget is NumericProxyProvider, + ); + testWidgets('ProxyProvider2', (tester) async { + await tester.pumpWidget( + MultiProvider( + providers: [ + Provider.value(value: a), + Provider.value(value: b), + Provider.value(value: c), + Provider.value(value: d), + Provider.value(value: e), + Provider.value(value: f), + ProxyProvider2( + initialBuilder: (_) => Combined(null, null, null), + builder: (context, a, b, previous) => + Combined(context, previous, a, b), + ) + ], + child: mockConsumer, + ), + ); + + final context = + tester.element(findProxyProvider()); + + verify( + combinedConsumerMock( + Combined(context, Combined(null, null, null), a, b), + ), + ).called(1); + }); + testWidgets('ProxyProvider3', (tester) async { + await tester.pumpWidget( + MultiProvider( + providers: [ + Provider.value(value: a), + Provider.value(value: b), + Provider.value(value: c), + Provider.value(value: d), + Provider.value(value: e), + Provider.value(value: f), + ProxyProvider3( + initialBuilder: (_) => Combined(null, null, null), + builder: (context, a, b, c, previous) => + Combined(context, previous, a, b, c), + ) + ], + child: mockConsumer, + ), + ); + + final context = + tester.element(findProxyProvider()); + + verify( + combinedConsumerMock( + Combined(context, Combined(null, null, null), a, b, c), + ), + ).called(1); + }); + testWidgets('ProxyProvider4', (tester) async { + await tester.pumpWidget( + MultiProvider( + providers: [ + Provider.value(value: a), + Provider.value(value: b), + Provider.value(value: c), + Provider.value(value: d), + Provider.value(value: e), + Provider.value(value: f), + ProxyProvider4( + initialBuilder: (_) => Combined(null, null, null), + builder: (context, a, b, c, d, previous) => + Combined(context, previous, a, b, c, d), + ) + ], + child: mockConsumer, + ), + ); + + final context = + tester.element(findProxyProvider()); + + verify( + combinedConsumerMock( + Combined(context, Combined(null, null, null), a, b, c, d), + ), + ).called(1); + }); + testWidgets('ProxyProvider5', (tester) async { + await tester.pumpWidget( + MultiProvider( + providers: [ + Provider.value(value: a), + Provider.value(value: b), + Provider.value(value: c), + Provider.value(value: d), + Provider.value(value: e), + Provider.value(value: f), + ProxyProvider5( + initialBuilder: (_) => Combined(null, null, null), + builder: (context, a, b, c, d, e, previous) => + Combined(context, previous, a, b, c, d, e), + ) + ], + child: mockConsumer, + ), + ); + + final context = tester.element(findProxyProvider()); + + verify( + combinedConsumerMock( + Combined(context, Combined(null, null, null), a, b, c, d, e, null), + ), + ).called(1); + }); + testWidgets('ProxyProvider6', (tester) async { + await tester.pumpWidget( + MultiProvider( + providers: [ + Provider.value(value: a), + Provider.value(value: b), + Provider.value(value: c), + Provider.value(value: d), + Provider.value(value: e), + Provider.value(value: f), + ProxyProvider6( + initialBuilder: (_) => Combined(null, null, null), + builder: (context, a, b, c, d, e, f, previous) => + Combined(context, previous, a, b, c, d, e, f), + ) + ], + child: mockConsumer, + ), + ); + + final context = tester.element(findProxyProvider()); + + verify( + combinedConsumerMock( + Combined(context, Combined(null, null, null), a, b, c, d, e, f), + ), + ).called(1); + }); + }); +} diff --git a/packages/provider/packages/provider/test/readme_test.dart b/packages/provider/packages/provider/test/readme_test.dart new file mode 100644 index 0000000..37f58ea --- /dev/null +++ b/packages/provider/packages/provider/test/readme_test.dart @@ -0,0 +1,17 @@ +import 'dart:io'; + +import 'package:flutter_test/flutter_test.dart'; + +void main() { + test('root/package/provider/README.md and root/README.mf are identical', + () async { + final root = await File.fromUri(Uri.parse( + '${Directory.current.parent.parent.parent.path}/README.md')) + .readAsString(); + final local = await File.fromUri( + Uri.parse('${Directory.current.parent.path}/README.md')) + .readAsString(); + + expect(root, equals(local)); + }); +} diff --git a/packages/provider/packages/provider/test/stateful_provider_test.dart b/packages/provider/packages/provider/test/stateful_provider_test.dart new file mode 100644 index 0000000..e866bcc --- /dev/null +++ b/packages/provider/packages/provider/test/stateful_provider_test.dart @@ -0,0 +1,74 @@ +import 'package:flutter/widgets.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:provider/provider.dart'; +import 'package:mockito/mockito.dart'; +import 'package:provider/src/provider.dart'; + +class ValueBuilder extends Mock { + int call(BuildContext context); +} + +class Dispose extends Mock { + void call(BuildContext context, int value); +} + +void main() { + test('cloneWithChild works', () { + final provider = Provider( + builder: (_) => 42, + child: Container(), + key: const ValueKey(42), + ); + + final newChild = Container(); + final clone = provider.cloneWithChild(newChild); + expect(clone.child, equals(newChild)); + // ignore: invalid_use_of_protected_member + expect(clone.delegate, equals(provider.delegate)); + expect(clone.key, equals(provider.key)); + expect(provider.updateShouldNotify, equals(clone.updateShouldNotify)); + }); + test('asserts', () { + expect( + () => Provider(builder: null, child: null), + throwsAssertionError, + ); + // don't throw + Provider(builder: (_) => null, child: null); + }); + + testWidgets('calls builder only once', (tester) async { + final builder = ValueBuilder(); + await tester.pumpWidget(Provider( + builder: builder, + child: Container(), + )); + await tester.pumpWidget(Provider( + builder: builder, + child: Container(), + )); + await tester.pumpWidget(Container()); + + verify(builder(any)).called(1); + }); + + testWidgets('dispose', (tester) async { + final dispose = Dispose(); + const key = ValueKey(42); + + await tester.pumpWidget( + Provider( + key: key, + builder: (_) => 42, + dispose: dispose, + child: Container(), + ), + ); + + final context = tester.element(find.byKey(key)); + + verifyZeroInteractions(dispose); + await tester.pumpWidget(Container()); + verify(dispose(context, 42)).called(1); + }); +} diff --git a/packages/provider/packages/provider/test/stream_provider_test.dart b/packages/provider/packages/provider/test/stream_provider_test.dart new file mode 100644 index 0000000..1e202fe --- /dev/null +++ b/packages/provider/packages/provider/test/stream_provider_test.dart @@ -0,0 +1,377 @@ +import 'dart:async'; + +import 'package:flutter/widgets.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/mockito.dart'; +import 'package:provider/provider.dart'; + +import 'common.dart'; + +class ErrorBuilderMock extends Mock { + T call(BuildContext context, Object error); +} + +class MockStreamController extends Mock implements StreamController {} + +class MockStream extends Mock implements Stream {} + +void main() { + group('streamProvider', () { + testWidgets('update when value change (default) ', (tester) async { + final controller = StreamController(); + final providerKey = GlobalKey(); + final childKey = GlobalKey(); + BuildContext context; + + await tester.pumpWidget(StreamProvider( + key: providerKey, + builder: (c) { + context = c; + return controller.stream; + }, + child: Container(key: childKey), + )); + + expect(context, equals(providerKey.currentContext)); + expect(Provider.of(childKey.currentContext), null); + + controller.add(0); + // adding to stream is asynchronous so we have to delay the pump + await Future.microtask(tester.pump); + + expect(Provider.of(childKey.currentContext), 0); + }); + testWidgets('update when value change (.value)', (tester) async { + final controller = StreamController(); + final key = GlobalKey(); + + await tester.pumpWidget(StreamProvider.value( + value: controller.stream, + child: Container(key: key), + )); + + expect(Provider.of(key.currentContext), null); + + controller.add(0); + // adding to stream is asynchronous so we have to delay the pump + await Future.microtask(tester.pump); + + expect(Provider.of(key.currentContext), 0); + }); + + testWidgets("don't notify descendants when rebuilding by default", + (tester) async { + final controller = StreamController(); + + final builder = BuilderMock(); + when(builder(any)).thenAnswer((invocation) { + final context = invocation.positionalArguments.first as BuildContext; + Provider.of(context); + return Container(); + }); + final child = Builder(builder: builder); + + await tester.pumpWidget(StreamProvider.value( + value: controller.stream, + child: child, + )); + + await tester.pumpWidget(StreamProvider.value( + value: controller.stream, + child: child, + )); + + verify(builder(any)).called(1); + }); + + testWidgets('pass down keys', (tester) async { + final controller = StreamController(); + final key = GlobalKey(); + + await tester.pumpWidget(StreamProvider.value( + key: key, + value: controller.stream, + child: Container(), + )); + + expect(key.currentWidget, isInstanceOf()); + }); + + testWidgets('pass updateShouldNotify', (tester) async { + final shouldNotify = UpdateShouldNotifyMock(); + when(shouldNotify(null, 1)).thenReturn(true); + + final controller = StreamController(); + await tester.pumpWidget(StreamProvider.value( + value: controller.stream, + updateShouldNotify: shouldNotify, + child: Container(), + )); + + verifyZeroInteractions(shouldNotify); + + controller.add(1); + // adding to stream is asynchronous so we have to delay the pump + await Future.microtask(tester.pump); + + verify(shouldNotify(null, 1)).called(1); + verifyNoMoreInteractions(shouldNotify); + }); + + testWidgets("don't listen again if stream instance doesn't change", + (tester) async { + final stream = MockStream(); + await tester.pumpWidget(StreamProvider.value( + value: stream, + child: Container(), + )); + await tester.pumpWidget(StreamProvider.value( + value: stream, + child: Container(), + )); + + verify( + stream.listen(any, + onError: anyNamed('onError'), + onDone: anyNamed('onDone'), + cancelOnError: anyNamed('cancelOnError')), + ).called(1); + verifyNoMoreInteractions(stream); + }); + + testWidgets('throws if stream has error and catchError is missing', + (tester) async { + final controller = StreamController(); + + await tester.pumpWidget(StreamProvider.value( + value: controller.stream, + child: Container(), + )); + + controller.addError(42); + + await Future.microtask(tester.pump); + final exception = tester.takeException() as Object; + expect(exception, isFlutterError); + expect(exception.toString(), equals(''' +An exception was throw by _ControllerStream listened by +StreamProvider, but no `catchError` was provided. + +Exception: +42 +''')); + }); + testWidgets('calls catchError if present and stream has error', + (tester) async { + final controller = StreamController(); + final key = GlobalKey(); + final catchError = ErrorBuilderMock(); + when(catchError(any, 42)).thenReturn(0); + + await tester.pumpWidget(StreamProvider.value( + value: controller.stream, + catchError: catchError, + child: Container(key: key), + )); + + controller.addError(42); + + await Future.microtask(tester.pump); + + expect(Provider.of(key.currentContext), 0); + + final context = findElementOfWidget>(); + + verify(catchError(context, 42)); + }); + + testWidgets('works with MultiProvider', (tester) async { + final key = GlobalKey(); + await tester.pumpWidget(MultiProvider( + providers: [ + StreamProvider.value(value: const Stream.empty()), + ], + child: Container(key: key), + )); + + expect(Provider.of(key.currentContext), null); + }); + test('works with MultiProvider #2', () { + final provider = StreamProvider.value( + value: const Stream.empty(), + initialData: 42, + child: Container(), + catchError: (_, __) => 42, + key: const Key('42'), + updateShouldNotify: (_, __) => true, + ); + var child2 = Container(); + final clone = provider.cloneWithChild(child2); + + expect(clone.child, equals(child2)); + expect(clone.updateShouldNotify, equals(provider.updateShouldNotify)); + expect(clone.key, equals(provider.key)); + expect(clone.initialData, equals(provider.initialData)); + // ignore: invalid_use_of_protected_member + expect(clone.delegate, equals(provider.delegate)); + expect(clone.catchError, equals(provider.catchError)); + }); + test('works with MultiProvider #3', () { + final provider = StreamProvider.controller( + builder: (_) => StreamController(), + initialData: 42, + child: Container(), + catchError: (_, __) => 42, + key: const Key('42'), + updateShouldNotify: (_, __) => true, + ); + var child2 = Container(); + final clone = provider.cloneWithChild(child2); + + expect(clone.child, equals(child2)); + expect(clone.updateShouldNotify, equals(provider.updateShouldNotify)); + expect(clone.key, equals(provider.key)); + expect(clone.initialData, equals(provider.initialData)); + // ignore: invalid_use_of_protected_member + expect(clone.delegate, equals(provider.delegate)); + expect(clone.catchError, equals(provider.catchError)); + }); + testWidgets('works with null', (tester) async { + final key = GlobalKey(); + await tester.pumpWidget(StreamProvider.value( + value: null, + child: Container(key: key), + )); + + expect(Provider.of(key.currentContext), null); + }); + + group('stateful constructor', () { + test('crashes if builder is null', () { + expect( + () => StreamProvider.controller(builder: null), + throwsAssertionError, + ); + }); + + testWidgets('works with null', (tester) async { + final key = GlobalKey(); + await tester.pumpWidget(StreamProvider.controller( + builder: (_) => null, + child: Container(key: key), + )); + + expect(Provider.of(key.currentContext), null); + + await tester.pumpWidget(Container()); + }); + + testWidgets('create and dispose stream with builder', (tester) async { + final realController = StreamController(); + final controller = MockStreamController(); + when(controller.stream).thenAnswer((_) => realController.stream); + + final builder = ValueBuilderMock>(); + when(builder(any)).thenReturn(controller); + + await tester.pumpWidget(StreamProvider.controller( + builder: builder, + child: Container(), + )); + + final context = findElementOfWidget>(); + + verify(builder(context)).called(1); + clearInteractions(controller); + + // extra build to see if builder isn't called again + await tester.pumpWidget(StreamProvider.controller( + builder: builder, + child: Container(), + )); + + await tester.pumpWidget(Container()); + + verifyNoMoreInteractions(builder); + verify(controller.close()); + }); + + testWidgets('pass updateShouldNotify', (tester) async { + final shouldNotify = UpdateShouldNotifyMock(); + when(shouldNotify(null, 1)).thenReturn(true); + + var controller = StreamController(); + await tester.pumpWidget(StreamProvider.controller( + builder: (_) => controller, + updateShouldNotify: shouldNotify, + child: Container(), + )); + + verifyZeroInteractions(shouldNotify); + + controller.add(1); + // adding to stream is asynchronous so we have to delay the pump + await Future.microtask(tester.pump); + + verify(shouldNotify(null, 1)).called(1); + verifyNoMoreInteractions(shouldNotify); + }); + + testWidgets( + 'Changing from default to stateful constructor calls stateful builder', + (tester) async { + final key = GlobalKey(); + final controller = StreamController(); + await tester.pumpWidget(StreamProvider.value( + value: controller.stream, + child: Container(), + )); + + final realController2 = StreamController(); + final controller2 = MockStreamController(); + when(controller2.stream).thenAnswer((_) => realController2.stream); + + realController2.add(42); + + await tester.pumpWidget(StreamProvider.controller( + builder: (_) => controller2, + child: Container(key: key), + )); + + await tester.pump(); + + expect(Provider.of(key.currentContext), 42); + + await tester.pumpWidget(Container()); + + verify(controller2.close()).called(1); + }); + testWidgets( + 'Changing from stateful to default constructor dispose correctly stateful stream', + (tester) async { + final realController = StreamController(); + final controller = MockStreamController(); + when(controller.stream).thenAnswer((_) => realController.stream); + + final key = GlobalKey(); + + await tester.pumpWidget(StreamProvider.controller( + builder: (_) => controller, + child: Container(), + )); + + await tester.pumpWidget(StreamProvider.value( + value: Stream.fromIterable([42]), + child: Container(key: key), + )); + await tester.pump(); + + expect(Provider.of(key.currentContext), 42); + + await tester.pumpWidget(Container()); + + verify(controller.close()).called(1); + }); + }); + }); +} diff --git a/packages/provider/packages/provider/test/value_listenable_provider_test.dart b/packages/provider/packages/provider/test/value_listenable_provider_test.dart new file mode 100644 index 0000000..fdf20ed --- /dev/null +++ b/packages/provider/packages/provider/test/value_listenable_provider_test.dart @@ -0,0 +1,162 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/mockito.dart'; +import 'package:provider/provider.dart'; +import 'common.dart'; + +class ValueNotifierMock extends Mock implements ValueNotifier {} + +void main() { + group('valueListenableProvider', () { + testWidgets( + 'disposing ValueListenableProvider on a builder constructor disposes of the ValueNotifier', + (tester) async { + final mock = ValueNotifierMock(); + await tester.pumpWidget(ValueListenableProvider( + builder: (_) => mock, + child: Container(), + )); + + final listener = + verify(mock.addListener(captureAny)).captured.first as VoidCallback; + + clearInteractions(mock); + await tester.pumpWidget(Container()); + verifyInOrder([ + mock.removeListener(listener), + mock.dispose(), + ]); + verifyNoMoreInteractions(mock); + }); + testWidgets('rebuilds when value change', (tester) async { + final listenable = ValueNotifier(0); + + final child = Builder( + builder: (context) => Text(Provider.of(context).toString(), + textDirection: TextDirection.ltr)); + + await tester.pumpWidget(ValueListenableProvider.value( + value: listenable, + child: child, + )); + + expect(find.text('0'), findsOneWidget); + listenable.value++; + await tester.pump(); + expect(find.text('1'), findsOneWidget); + }); + + testWidgets("don't rebuild dependents by default", (tester) async { + final builder = BuilderMock(); + when(builder(any)).thenAnswer((invocation) { + final context = invocation.positionalArguments.first as BuildContext; + Provider.of(context); + return Container(); + }); + + final listenable = ValueNotifier(0); + final child = Builder(builder: builder); + + await tester.pumpWidget(ValueListenableProvider.value( + value: listenable, + child: child, + )); + verify(builder(any)).called(1); + + await tester.pumpWidget(ValueListenableProvider.value( + value: listenable, + child: child, + )); + verifyNoMoreInteractions(builder); + }); + + testWidgets('pass keys', (tester) async { + final key = GlobalKey(); + await tester.pumpWidget(ValueListenableProvider.value( + key: key, + value: ValueNotifier(42), + child: Container(), + )); + + expect(key.currentWidget, isInstanceOf>()); + }); + + testWidgets("don't listen again if stream instance doesn't change", + (tester) async { + final valueNotifier = ValueNotifierMock(); + await tester.pumpWidget(ValueListenableProvider.value( + value: valueNotifier, + child: Container(), + )); + await tester.pumpWidget(ValueListenableProvider.value( + value: valueNotifier, + child: Container(), + )); + + verify(valueNotifier.addListener(any)).called(1); + verify(valueNotifier.value); + verifyNoMoreInteractions(valueNotifier); + }); + testWidgets('pass updateShouldNotify', (tester) async { + final shouldNotify = UpdateShouldNotifyMock(); + when(shouldNotify(0, 1)).thenReturn(true); + + var notifier = ValueNotifier(0); + await tester.pumpWidget(ValueListenableProvider.value( + value: notifier, + updateShouldNotify: shouldNotify, + child: Container(), + )); + + verifyZeroInteractions(shouldNotify); + + notifier.value++; + await tester.pump(); + + verify(shouldNotify(0, 1)).called(1); + verifyNoMoreInteractions(shouldNotify); + }); + + testWidgets('works with MultiProvider', (tester) async { + final key = GlobalKey(); + await tester.pumpWidget(MultiProvider( + providers: [ValueListenableProvider.value(value: ValueNotifier(42))], + child: Container(key: key), + )); + + expect(Provider.of(key.currentContext), 42); + }); + + test('works with MultiProvider #2', () { + final provider = ValueListenableProvider.value( + key: const Key('42'), + value: ValueNotifier(42), + child: Container(), + ); + var child2 = Container(); + final clone = provider.cloneWithChild(child2); + + expect(clone.child, equals(child2)); + expect(clone.key, equals(provider.key)); + // ignore: invalid_use_of_protected_member + expect(clone.delegate, equals(provider.delegate)); + expect(clone.updateShouldNotify, equals(provider.updateShouldNotify)); + }); + test('works with MultiProvider #3', () { + final provider = ValueListenableProvider( + builder: (_) => ValueNotifier(42), + child: Container(), + key: const Key('42'), + ); + var child2 = Container(); + final clone = provider.cloneWithChild(child2); + + expect(clone.child, equals(child2)); + expect(clone.key, equals(provider.key)); + // ignore: invalid_use_of_protected_member + expect(clone.delegate, equals(provider.delegate)); + expect(clone.updateShouldNotify, equals(provider.updateShouldNotify)); + }); + }); +} diff --git a/packages/provider/scripts/flutter_test.sh b/packages/provider/scripts/flutter_test.sh new file mode 100755 index 0000000..5832c1d --- /dev/null +++ b/packages/provider/scripts/flutter_test.sh @@ -0,0 +1,7 @@ +cd $1 +flutter packages get +flutter format --set-exit-if-changed lib test +flutter analyze --no-current-package lib test/ +flutter test --no-pub --coverage +# resets to the original state +cd - \ No newline at end of file