From 3e5c725dcba3572f281aa5265270e2c87f2fe777 Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Tue, 5 Feb 2019 00:08:12 +0100 Subject: [PATCH] Reintroduced StatefulProvider (#19) --- CHANGELOG.md | 6 + README.md | 29 +++++ example/stateful_provider.dart | 3 +- lib/provider.dart | 7 +- lib/src/provider.dart | 180 ++++++++++++++++++-------- lib/src/provider.g.dart | 55 -------- pubspec.yaml | 6 +- test/stateful_provider_test.dart | 215 ++++++------------------------- 8 files changed, 204 insertions(+), 297 deletions(-) delete mode 100644 lib/src/provider.g.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index 1d1c0f36..f73f2811 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +# 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. diff --git a/README.md b/README.md index 9a258ad1..9617b8c0 100644 --- a/README.md +++ b/README.md @@ -69,6 +69,35 @@ Provider( ) ``` +### StatefulProvider + +A [Provider] that can also create and dispose an object. + +It is usually used to avoid making a [StatefulWidget] for something trivial, such as instanciating a BLoC. + +[StatefulBuilder] is the equivalent of a [State.initState] combined with [State.dispose]. +As such, [valueBuilder] is called only once and is unable to use [InheritedWidget]; which makes it impossible to update the created value. + +If this is too limiting, consider instead [HookProvider], which offer a much more advanced control over the created value. + +The following example instanciate a `Model` once, and dispose it when [StatefulProvider] is removed from the tree. + +```dart +class Model { + void dispose() {} +} + +class Stateless extends StatelessWidget { + @override + Widget build(BuildContext context) { + return StatefulProvider( + valueBuilder: (context) => Model(), + dispose: (context, value) => value.dispose(), + child: ..., + ); + } +} +``` ### HookProvider diff --git a/example/stateful_provider.dart b/example/stateful_provider.dart index bd5719fd..1ec25736 100644 --- a/example/stateful_provider.dart +++ b/example/stateful_provider.dart @@ -23,9 +23,8 @@ class Bloc { class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { - // ignore: deprecated_member_use return StatefulProvider( - valueBuilder: (_, old) => old ?? Bloc(), + valueBuilder: (_) => Bloc(), onDispose: (_, value) => value.dipose(), child: Example(), ); diff --git a/lib/provider.dart b/lib/provider.dart index d24abb8e..49fcf4e2 100644 --- a/lib/provider.dart +++ b/lib/provider.dart @@ -1,9 +1,4 @@ library provider; export 'src/provider.dart' - show - Provider, -// ignore: deprecated_member_use, deprecated_member_use_from_same_package - StatefulProvider, - HookProvider, - Consumer; + show Provider, StatefulProvider, HookProvider, Consumer; diff --git a/lib/src/provider.dart b/lib/src/provider.dart index 374ecff7..669d7404 100644 --- a/lib/src/provider.dart +++ b/lib/src/provider.dart @@ -1,10 +1,6 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:functional_widget_annotation/functional_widget_annotation.dart' - hide widget; - -part 'provider.g.dart'; /// Necessary to obtain generic [Type] /// see https://stackoverflow.com/questions/52891537/how-to-get-generic-type @@ -70,6 +66,12 @@ class Provider extends InheritedWidget { } } +@visibleForTesting +// ignore: public_member_api_docs +UpdateShouldNotify debugGetProviderUpdateShouldNotify( + Provider provider) => + provider._updateShouldNotify; + /// 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). @@ -90,38 +92,40 @@ class Consumer extends StatelessWidget { } } -/// A wrapper over [Provider] to make exposing complex objets +/// A [Provider] that can also create and dispose an object. +/// +/// It is usually used to avoid making a [StatefulWidget] for something trivial, such as instanciating a BLoC. +/// +/// [StatefulBuilder] is the equivalent of a [State.initState] combined with [State.dispose]. +/// As such, [valueBuilder] is called only once and is unable to use [InheritedWidget]; which makes it impossible to update the created value. +/// +/// If this is too limiting, consider instead [HookProvider], which offer a much more advanced control over the created value. /// -/// It is usuallt used to create once an object, to not recreate it on every [State.build] call -/// without having to manually create a [StatefulWidget] +/// The following example instanciate a `Model` once, and dispose it when [StatefulProvider] is removed from the tree. /// /// ``` -/// class Model {} +/// class Model { +/// void dispose() {} +/// } /// /// class Stateless extends StatelessWidget { /// @override /// Widget build(BuildContext context) { /// return StatefulProvider( -/// valueBuilder: (context, old) => old ?? Model(), +/// valueBuilder: (context) => Model(), +/// dispose: (context, value) => value.dispose(), /// child: ..., /// ); /// } /// } /// ``` -@Deprecated('Prefer HookBuilder combined with useState hook instead') class StatefulProvider extends StatefulWidget { - /// [valueBuilder] is called on [State.initState] and [State.didUpdateWidget] + /// A function that creates the provided value. /// - /// The second argument of [valueBuilder] is the previous value returned by [valueBuilder]. - /// This value will be `null` on the first call. + /// [valueBuilder] must not be null and is called only once for the life-cycle of [StatefulProvider]. /// /// It is not possible to obtain an [InheritedWidget] from [valueBuilder]. - /// Use [didChangeDependencies] instead. - final T Function(BuildContext context, T previous) valueBuilder; - - /// [didChangeDependencies] is a hook to [State.didChangeDependencies] - /// It can be used to build/update values depending on an [InheritedWidget] - final T Function(BuildContext context, T value) didChangeDependencies; + final T Function(BuildContext context) valueBuilder; /// [onDispose] is a callback called when [StatefulProvider] is /// removed for the widget tree, and pass the current value as parameter. @@ -140,40 +144,24 @@ class StatefulProvider extends StatefulWidget { /// Allows to specify parameters to [StatefulProvider] StatefulProvider({ Key key, - this.valueBuilder, - this.child, + @required this.valueBuilder, + @required this.child, this.onDispose, - this.didChangeDependencies, this.updateShouldNotify, - }) : assert(valueBuilder != null || didChangeDependencies != null), + }) : assert(valueBuilder != null), super(key: key); @override _StatefulProviderState createState() => _StatefulProviderState(); } -@deprecated class _StatefulProviderState extends State> { T _value; @override void initState() { super.initState(); - _buildValue(); - } - - @override - void didUpdateWidget(StatefulProvider oldWidget) { - super.didUpdateWidget(oldWidget); - _buildValue(); - } - - @override - void didChangeDependencies() { - super.didChangeDependencies(); - if (widget.didChangeDependencies != null) { - _value = widget.didChangeDependencies(context, _value); - } + _value = widget.valueBuilder(context); } @override @@ -192,25 +180,103 @@ class _StatefulProviderState extends State> { child: widget.child, ); } - - void _buildValue() { - if (widget.valueBuilder != null) { - _value = widget.valueBuilder(context, _value); - } - } } -/// A [Provider] that exposes a value obtained from a [Hook]. +/// A provider which can use hooks from [flutter_hooks](https://github.com/rrousselGit/flutter_hooks) /// -/// [HookProvider] will rebuild and potentially expose a new value if the hooks used ask for it. -@hwidget -Widget hookProvider( - {T hook(), - @required Widget child, - UpdateShouldNotify updateShouldNotify}) { - return Provider( - value: hook(), - child: child, - updateShouldNotify: updateShouldNotify, - ); +/// This is especially useful to create complex providers, without having to make a `StatefulWidget`. +/// +/// The following example uses BLoC pattern to create a BLoC, provide its value, and dispose it when the provider is removed from the tree. +/// +/// ```dart +/// HookProvider( +/// hook: () { +/// final bloc = useMemoized(() => MyBloc()); +/// useEffect(() => bloc.dispose, [bloc]); +/// return bloc; +/// }, +/// child: // ... +/// ) +/// ``` +class HookProvider extends HookWidget { + /// A provider which can use hooks from [flutter_hooks](https://github.com/rrousselGit/flutter_hooks) + /// + /// This is especially useful to create complex providers, without having to make a `StatefulWidget`. + /// + /// The following example uses BLoC pattern to create a BLoC, provide its value, and dispose it when the provider is removed from the tree. + /// + /// ```dart + /// HookProvider( + /// hook: () { + /// final bloc = useMemoized(() => MyBloc()); + /// useEffect(() => bloc.dispose, [bloc]); + /// return bloc; + /// }, + /// child: // ... + /// ) + /// ``` + const HookProvider( + {Key key, this.hook, @required this.child, this.updateShouldNotify}) + : super(key: key); + + /// A provider which can use hooks from [flutter_hooks](https://github.com/rrousselGit/flutter_hooks) + /// + /// This is especially useful to create complex providers, without having to make a `StatefulWidget`. + /// + /// The following example uses BLoC pattern to create a BLoC, provide its value, and dispose it when the provider is removed from the tree. + /// + /// ```dart + /// HookProvider( + /// hook: () { + /// final bloc = useMemoized(() => MyBloc()); + /// useEffect(() => bloc.dispose, [bloc]); + /// return bloc; + /// }, + /// child: // ... + /// ) + /// ``` + final T Function() hook; + + /// A provider which can use hooks from [flutter_hooks](https://github.com/rrousselGit/flutter_hooks) + /// + /// This is especially useful to create complex providers, without having to make a `StatefulWidget`. + /// + /// The following example uses BLoC pattern to create a BLoC, provide its value, and dispose it when the provider is removed from the tree. + /// + /// ```dart + /// HookProvider( + /// hook: () { + /// final bloc = useMemoized(() => MyBloc()); + /// useEffect(() => bloc.dispose, [bloc]); + /// return bloc; + /// }, + /// child: // ... + /// ) + /// ``` + final Widget child; + + /// A provider which can use hooks from [flutter_hooks](https://github.com/rrousselGit/flutter_hooks) + /// + /// This is especially useful to create complex providers, without having to make a `StatefulWidget`. + /// + /// The following example uses BLoC pattern to create a BLoC, provide its value, and dispose it when the provider is removed from the tree. + /// + /// ```dart + /// HookProvider( + /// hook: () { + /// final bloc = useMemoized(() => MyBloc()); + /// useEffect(() => bloc.dispose, [bloc]); + /// return bloc; + /// }, + /// child: // ... + /// ) + /// ``` + final bool Function(T, T) updateShouldNotify; + + @override + Widget build(BuildContext context) => Provider( + value: hook(), + child: child, + updateShouldNotify: updateShouldNotify, + ); } diff --git a/lib/src/provider.g.dart b/lib/src/provider.g.dart deleted file mode 100644 index 0f44ea61..00000000 --- a/lib/src/provider.g.dart +++ /dev/null @@ -1,55 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'provider.dart'; - -// ************************************************************************** -// Generator: FunctionalWidget -// ************************************************************************** - -/// A [Provider] that exposes a value obtained from a [Hook]. -/// -/// [HookProvider] will rebuild and potentially expose a new value if the hooks used ask for it. -class HookProvider extends HookWidget { - /// A [Provider] that exposes a value obtained from a [Hook]. - /// - /// [HookProvider] will rebuild and potentially expose a new value if the hooks used ask for it. - const HookProvider( - {Key key, this.hook, @required this.child, this.updateShouldNotify}) - : super(key: key); - - /// A [Provider] that exposes a value obtained from a [Hook]. - /// - /// [HookProvider] will rebuild and potentially expose a new value if the hooks used ask for it. - final T Function() hook; - - /// A [Provider] that exposes a value obtained from a [Hook]. - /// - /// [HookProvider] will rebuild and potentially expose a new value if the hooks used ask for it. - final Widget child; - - /// A [Provider] that exposes a value obtained from a [Hook]. - /// - /// [HookProvider] will rebuild and potentially expose a new value if the hooks used ask for it. - final bool Function(T, T) updateShouldNotify; - - @override - Widget build(BuildContext _context) => hookProvider( - hook: hook, child: child, updateShouldNotify: updateShouldNotify); - @override - int get hashCode => hashValues(hook, child, updateShouldNotify); - @override - bool operator ==(Object o) => - identical(o, this) || - (o is HookProvider && - hook == o.hook && - child == o.child && - updateShouldNotify == o.updateShouldNotify); - @override - void debugFillProperties(DiagnosticPropertiesBuilder properties) { - super.debugFillProperties(properties); - properties.add(ObjectFlagProperty.has('hook', hook)); - properties.add(DiagnosticsProperty('child', child)); - properties.add(ObjectFlagProperty.has( - 'updateShouldNotify', updateShouldNotify)); - } -} diff --git a/pubspec.yaml b/pubspec.yaml index 539073d0..1816871a 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: provider description: An helper to easily exposes a value using InheritedWidget without having to write one. -version: 1.3.0 +version: 1.4.0 homepage: https://github.com/rrousselGit/provider author: Remi Rousselet @@ -11,11 +11,9 @@ dependencies: flutter: sdk: flutter flutter_hooks: '>=0.2.1 <1.0.0' - functional_widget_annotation: '>=0.3.0 <1.0.0' dev_dependencies: - functional_widget: ^0.4.0 - build_runner: ^1.1.2 pedantic: ^1.4.0 + mockito: ^4.0.0 flutter_test: sdk: flutter diff --git a/test/stateful_provider_test.dart b/test/stateful_provider_test.dart index 12c3a439..52730b5e 100644 --- a/test/stateful_provider_test.dart +++ b/test/stateful_provider_test.dart @@ -1,208 +1,77 @@ -// ignore_for_file: deprecated_member_use, deprecated_member_use_from_same_package - 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'; -void main() { - test('assets', () { - expect(() => StatefulProvider(), throwsAssertionError); - expect( - () => StatefulProvider(didChangeDependencies: (_, dynamic __) => null), - isNot(throwsAssertionError)); - expect(() => StatefulProvider(valueBuilder: (_, dynamic __) => null), - isNot(throwsAssertionError)); - expect( - () => StatefulProvider( - valueBuilder: (_, dynamic __) => null, - didChangeDependencies: (_, dynamic __) => null, - ), - isNot(throwsAssertionError)); - }); - - testWidgets('simple usage', (tester) async { - var buildCount = 0; - int previous; - int value; - BuildContext context; - - final valueBuilder = (BuildContext c, int p) { - previous = p; - context = c; - return ++buildCount; - }; - - final builder = Builder( - builder: (context) { - value = Provider.of(context); - return Container(); - }, - ); - - await tester.pumpWidget( - StatefulProvider( - valueBuilder: valueBuilder, - child: builder, - ), - ); - - final element = tester.element(find.byElementType(StatefulElement).first); +class ValueBuilder extends Mock { + int call(BuildContext context); +} - expect(buildCount, equals(1)); - expect(previous, isNull); - expect(value, equals(1)); - expect(element, equals(context)); +class Dispose extends Mock { + void call(BuildContext context, int value); +} - await tester.pumpWidget( - StatefulProvider( - valueBuilder: valueBuilder, - child: builder, - ), +void main() { + test('asserts', () { + expect( + () => StatefulProvider(valueBuilder: null, child: null), + throwsAssertionError, ); - - expect(buildCount, equals(2)); - expect(previous, equals(1)); - expect(value, equals(2)); - - // pump different widget to trigger dispose mecanism - // no `onDispose` has been provided: should handle null - await tester.pumpWidget(Container()); + // don't throw + StatefulProvider(valueBuilder: (_) => null, child: null); }); - testWidgets('didChangeDependencies', (tester) async { - var dependenciesCount = 0; - BuildContext context; - int value; - int previous; - - final dependencies = (BuildContext c, int v) { - context = c; - dependenciesCount++; - previous = v; - value = Provider.of(context).toInt(); - return value; - }; - - await tester.pumpWidget( - Provider( - value: 42.0, - child: StatefulProvider( - didChangeDependencies: dependencies, - child: Container(), - ), - ), - ); - - final element = tester.element(find.byElementType(StatefulElement).first); - - expect(dependenciesCount, equals(1)); - expect(previous, isNull); - expect(value, equals(42)); - expect(context, equals(element)); - - await tester.pumpWidget( - Provider( - value: 43.0, - child: StatefulProvider( - didChangeDependencies: dependencies, - child: Container(), - ), - ), - ); + testWidgets('calls valueBuilder only once', (tester) async { + final builder = ValueBuilder(); + await tester.pumpWidget(StatefulProvider( + valueBuilder: builder, + child: Container(), + )); + await tester.pumpWidget(StatefulProvider( + valueBuilder: builder, + child: Container(), + )); + await tester.pumpWidget(Container()); - expect(dependenciesCount, equals(2)); - expect(previous, equals(42)); - expect(value, equals(43)); - expect(context, equals(element)); + verify(builder(any)).called(1); }); testWidgets('dispose', (tester) async { - var disposeCount = 0; - int value; - BuildContext context; - - final dispose = (BuildContext c, int v) { - context = c; - disposeCount++; - value = v; - }; - - final valueBuilder = (BuildContext context, int previous) => 42; + final dispose = Dispose(); + const key = ValueKey(42); await tester.pumpWidget( StatefulProvider( - valueBuilder: valueBuilder, + key: key, + valueBuilder: (_) => 42, onDispose: dispose, child: Container(), ), ); - final element = tester.element(find.byElementType(StatefulElement).first); + final context = tester.element(find.byKey(key)); - await tester.pump(); - // pump different widget to trigger dispose mecanism + verifyZeroInteractions(dispose); await tester.pumpWidget(Container()); - - expect(disposeCount, equals(1)); - expect(value, equals(42)); - expect(context, equals(element)); + verify(dispose(context, 42)).called(1); }); 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(); - }); + final shouldNotify = (int a, int b) => true; await tester.pumpWidget( StatefulProvider( - valueBuilder: (_, dynamic __) => 24, - updateShouldNotify: updateShouldNotify, - child: builder, + valueBuilder: (_) => 42, + updateShouldNotify: shouldNotify, + child: Container(), ), ); - expect(callCount, equals(0)); - expect(buildCount, equals(1)); - expect(buildValue, equals(24)); - // value changed - await tester.pumpWidget( - StatefulProvider( - valueBuilder: (_, dynamic __) => 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)); + final provider = + tester.widget(find.byWidgetPredicate((w) => w is Provider)) + as Provider; - // value didnt' change - await tester.pumpWidget( - StatefulProvider( - valueBuilder: (_, dynamic __) => 25, - updateShouldNotify: updateShouldNotify, - child: builder, - ), - ); - expect(callCount, equals(2)); - expect(old, equals(25)); - expect(curr, equals(25)); - expect(buildCount, equals(2)); + expect(debugGetProviderUpdateShouldNotify(provider), shouldNotify); }); }